Gamma

Query and update violations in Gamma.

Network Security · Gamma

Details

IDGamma
ProviderGamma
CategoryNetwork Security
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Gamma Enterprise DLP provides 1-click automatic discovery and remediation of data loss instances
across enterprise sanctioned SaaS applications (cloud and on-prem) such as: Slack, Github, GSuite (Gmail, GDrive), Atlassian Suite (Jira, Confluence), Microsoft Office 365 (Outlook, Teams, OneDrive), ServiceNow, ZenDesk and many more.

Configure Gamma.AI Enterprise DLP on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Gamma.
  3. Click Add instance to create and configure a new integration instance.
Parameter Description Required
api_key Gamma Discovery API Key True
url URL of the Gamma API True
first_fetch The violation ID (offset) to begin fetching from. The value must be a number equal to or greater than 1. If empty, the fetch will default to the first violation that exists. You can retrieve a list of violation IDs by running the gamma-get-violation-list command. False
max_fetch Max results to return False
insecure Trust any certificate (not secure) False
proxy Use system proxy settings False

Click Test to validate the URLs, token, and connection.

Commands

You can execute these commands 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.

gamma-get-violation-list


Fetch DLP violations found across SaaS applications monitored by Gamma

Base Command

gamma-get-violation-list

Input

Argument Name Description Required
minimum_violation Violation ID to begin pulling from. Defaults to the earliest existing violation for your account. Required
limit Default is “10”. Required

Context Output

Path Type Description
GammaViolation.violation_id Integer Violation ID
GammaViolation.file_labels_map Array File in reference to the DLP violation
GammaViolation.violation_status String one of ‘OPEN’, ‘RESOLVED’, ‘IGNORED’
GammaViolation.violation_category String Category of the violation e.g. PII, Secrets, GDPR/CCPA, etc.
GammaViolation.violation_event_timestamp Integer Timestamp of violation in epoch milliseconds
GammaViolation.text_labels Array Data classification labels
GammaViolation.user JSON Object a JSON field containing optional information (based on what the app allows us to access) like email address, name, atlassian account id, AD id, github login, etc. All these fields are nullable.
GammaViolation.dashboard_url String Gamma dashboard URL
GammaViolation.app_name String Name of the application

Command Example

!gamma-get-violation-list minimum_violation=998 limit=1

Context Example

{
    "response": [
        {
            "violation_id": 999,
            "file_labels_map": {
                "svc-prod-account.json": [
                    "cloud_db_credential"
                ]
            },
            "violation_status": "OPEN",
            "violation_category": "secrets",
            "violation_event_timestamp": 1569550580,
            "text_labels": [],
            "user": {
                "name": null,
                "atlassian_account_id": null,
                "email_address": "foo@example.com",
                "active_directory_user_id": null,
                "atlassian_server_user_key": null,
                "slack_user_id": "USER9Aa2",
                "github_handle": "markzuck"
            },
            "dashboard_url": "https://prod-iab12.gamma.ai/dashboard/slack/monitor/violationId/999",
            "app_name": "slack"
        }]
}

gamma-get-violation


Fetches a single DLP violation. This command is the same as gamma-get-violation-list except that this
command only returns the DLP violation details of the given violation id.

Base Command

gamma-get-violation

Input

Argument Name Description Required
violation Violation id Required

Context Output

Path Type Description
GammaViolation.violation_id Integer Violation ID
GammaViolation.file_labels_map Array File in reference to the DLP violation
GammaViolation.violation_status String one of ‘OPEN’, ‘RESOLVED’, ‘IGNORED’
GammaViolation.violation_category String Category of the violation e.g. PII, Secrets, GDPR/CCPA, etc.
GammaViolation.violation_event_timestamp Integer Timestamp of violation in epoch milliseconds
GammaViolation.text_labels Array Data classification labels
GammaViolation.user JSON Object a JSON field containing optional information (based on what the app allows us to access) like email address, name, atlassian account id, AD id, github login, etc. All these fields are nullable.
GammaViolation.dashboard_url String Gamma dashboard URL
GammaViolation.app_name String Name of the application

Command Example

!gamma-get-violation violation=998

Context Example

{
    "response": [
        {
            "violation_id": 999,
            "file_labels_map": {
                "svc-prod-account.json": [
                    "cloud_db_credential"
                ]
            },
            "violation_status": "OPEN",
            "violation_category": "secrets",
            "violation_event_timestamp": 1569550580,
            "text_labels": [],
            "user": {
                "name": null,
                "atlassian_account_id": null,
                "email_address": "foo@example.com",
                "active_directory_user_id": null,
                "atlassian_server_user_key": null,
                "slack_user_id": "USER9Aa2",
                "github_handle": "markzuck"
            },
            "dashboard_url": "https://prod-iab12.gamma.ai/dashboard/slack/monitor/violationId/999",
            "app_name": "slack"
        }]
}

gamma-update-violation


Updates a DLP violation status in Gamma

Base Command

gamma-update-violation

Input

Argument Name Description Required
violation Violation id Required
status Status of violation Required
notes Notes for violation Optional

Context Output

There is no context output for this command

Configuration parameters

  • url — Server URL (e.g. https://example.net) (required)
  • api_key — Gamma API Key
  • credentials_api_key
  • first_fetch — Starting Violation
  • max_fetch — Results per fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • isFetch — Fetch incidents

Commands (3)

  • gamma-get-violation

    Get a specific violation.

  • gamma-get-violation-list

    Get a list of violations.

  • gamma-update-violation

    Update a specific violation.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

""" IMPORTS """

import json
import urllib3
from typing import Any
from enum import Enum

# Disable insecure warnings
urllib3.disable_warnings()

"""CONSTANTS"""

MAX_INCIDENTS_TO_FETCH = 100

""" CLIENT CLASS """


class Client(BaseClient):
    """Implements Gamma API"""

    def __init__(self, demisto):
        api_key = demisto.params().get("credentials_api_key", {}).get("password") or demisto.params()["api_key"]
        if not api_key:
            raise DemistoException("Gamma API Key must be provided.")
        headers = {"X-API-Key": api_key}
        base_url = urljoin(demisto.params()["url"], "/api/discovery/v1/")
        verify_certificate = not (demisto.params().get("insecure", False))
        proxy = demisto.params().get("proxy", False)

        super().__init__(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy)

    def get_violation_list(self, minimum_violation: int, limit: int) -> dict[str, Any]:
        """Gets dict of all violations starting from the minimum ID

        :type minimum_violation: int
        :param minimum_violation: unique violation ID to begin search

        :type limit: int
        :param limit: <=100, enforced by API
        """

        return self._http_request(
            method="GET", url_suffix="/violation/list", params={"minimum_violation_id": minimum_violation, "limit": limit}
        )

    def get_violation(self, violation: int) -> dict[str, Any]:
        """Get dict of violation by unique ID

        :type violation: int
        :param violation: unique violation ID
        """

        return self._http_request(
            method="GET", url_suffix="/violation/list", params={"minimum_violation_id": violation, "limit": 1}
        )

    def update_violation(self, violation: int, status: str, notes: str) -> dict[str, Any]:
        """Update a violation's status and notes

        :type violation: int
        :param violation: unique violation ID

        :type status: string
        :param status: status to mark the violation. options are 'OPEN', 'RESOLVED', 'IGNORED'

        :type notes: string
        :param notes: notes to update current notes for the violation
        """

        return self._http_request(
            method="PUT", url_suffix=f"/violation/{violation}", json_data={"violation_status": status, "notes": notes}
        )


class ViolationStatus(Enum):
    OPEN = "OPEN"
    RESOLVED = "RESOLVED"
    IGNORED = "IGNORED"


""" COMMANDS """


class Command:
    @staticmethod
    def get_violation_list(client: Client, args: dict[str, Any]) -> CommandResults:
        """
        :type client: Client
        :param client: Gamma client

        :param args: all command arguments, usually passed from demisto.args()
            args['name'] is used as input name

        :return:
            A CommandResults object that is then passed to return_results

        :rtype: ``CommandResults``
        """

        minimum_violation = args.get("minimum_violation", 1)
        limit = args.get("limit", 10)

        if int(minimum_violation) < 1:
            raise ValueError("minimum_violation must be greater than 0")
        if int(limit) < 1 or int(limit) > 100:
            raise ValueError("limit must be between 1 and 100")

        response = client.get_violation_list(minimum_violation, limit)
        violations = response["response"]

        note = ""
        if violations[0]["violation_id"] != int(minimum_violation):
            note += (
                "Violation with the minimum_violation ID does not exist. "
                "Showing violations pulled from the next available ID: "
                f'{violations[0]["violation_id"]} \r'
            )

        human_readable = get_human_readable(violations)

        return CommandResults(
            readable_output=human_readable,
            outputs_prefix="GammaViolation",
            outputs_key_field="violation_id",
            outputs=violations,
            raw_response=violations,
        )

    @staticmethod
    def get_violation(client: Client, args: dict[str, Any]) -> CommandResults:
        """
        :type client: Client
        :param client: Gamma client

        :param args: all command arguments, usually passed from demisto.args()
            args['name'] is used as input name

        :return:
            A CommandResults object that is then passed to return_results

        :rtype: ``CommandResults``
        """

        violation_id = args["violation"]

        if int(violation_id) < 1:
            raise ValueError("Violation must be greater than 0")

        response = client.get_violation(violation_id)
        violations = response["response"]

        if violations[0]["violation_id"] != int(violation_id):
            raise ValueError("Violation with this ID does not exist.")

        human_readable = get_human_readable(violations)

        return CommandResults(
            readable_output=human_readable,
            outputs_prefix="GammaViolation",
            outputs_key_field="violation_id",
            outputs=violations,
            raw_response=violations,
        )

    @staticmethod
    def update_violation(client: Client, args: dict[str, Any]) -> CommandResults:
        """
        :type client: Client
        :param client: Gamma client

        :param args: all command arguments, usually passed from demisto.args()
            args['name'] is used as input name

        :return:
            A CommandResults object that is then passed to return_results

        :rtype: ``CommandResults``
        """

        violation = args["violation"]
        status = args["status"].upper()
        notes = args["notes"]

        if int(violation) < 1:
            raise ValueError("Violation must be greater than 0")
        try:
            ViolationStatus(status)
        except ValueError:
            raise ValueError("Status must be one of the following: OPEN, RESOLVED, IGNORED")

        client.update_violation(violation, status, notes)

        response = client.get_violation(violation)
        updated_violation = response["response"]
        human_readable = get_human_readable(updated_violation)

        return CommandResults(
            readable_output=human_readable,
            outputs_prefix="GammaViolation",
            outputs_key_field="violation_id",
            outputs=updated_violation,
            raw_response=updated_violation,
        )

    @staticmethod
    def run(command, client, args):
        if command == "gamma-get-violation-list":
            return Command.get_violation_list(client, args)
        elif command == "gamma-get-violation":
            return Command.get_violation(client, args)
        elif command == "gamma-update-violation":
            return Command.update_violation(client, args)
        else:
            raise NotImplementedError(f'Command "{command}" is not implemented.')


def test_module(client: Client) -> str:
    """Tests API connectivity and authentication

    Returning 'ok' indicates that the integration works like it is supposed to and connection to
    the service is successful.

    Raises exceptions if something goes wrong.

    :type client: Client
    :param client: Gamma client

    :return: 'ok' if test passed, anything else will fail the test.
    :rtype: ``str``
    """

    try:
        client.get_violation_list(minimum_violation=1, limit=10)
    except DemistoException as e:
        if "UNAUTHORIZED" in str(e):
            return "Authorization Error: Make sure Gamma Discovery API Key is correctly set"
        else:
            raise e
    return "ok"


def fetch_incidents(client: Client, last_run_violation: dict, str_first_fetch_violation: str, str_max_results: str):
    """This function will run each interval (default 1 minute)

    :type client: client
    :param client: Gamma client

    :type last_run_violation: dict
    :param last_run_violation: last violation ID that was queried from Gamma

    :type str_first_fetch_violation: str
    :param str_first_fetch_violation: if last_violation is None, then begin from this violation ID

    :type str_max_results: str
    :param str_first_fetch_violation: the max number of violations to pull, bound by
    MAX_INCIDENTS_TO_FETCH
    """

    try:
        first_fetch_violation = int(str_first_fetch_violation)
        max_results = int(str_max_results)
    except ValueError:
        raise ValueError("first_fetch_violation and max_limit must be integers")

    if first_fetch_violation < 1:
        raise ValueError("first_fetch_violation must be equal to 1 or higher")
    if max_results < 1:
        max_results = 10
    elif max_results > MAX_INCIDENTS_TO_FETCH:
        max_results = MAX_INCIDENTS_TO_FETCH

    # get the last violation id fetched, if exists
    starting_violation = last_run_violation.get("starting_violation", first_fetch_violation)

    most_recent_violation = starting_violation
    incidents = []
    violations = client.get_violation_list(starting_violation, max_results)

    for item in violations["response"]:
        incident_violation = item["violation_id"]
        incident_time_ms = item["violation_event_timestamp"] * 1000

        if incident_violation <= most_recent_violation:
            continue

        incident = {
            "name": f"Gamma Violation {incident_violation}",
            "occurred": timestamp_to_datestring(incident_time_ms),
            "rawJSON": json.dumps(item),
        }

        incidents.append(incident)

        # update last run if violation id is greater than last fetch
        if incident_violation > most_recent_violation:
            most_recent_violation = incident_violation

    next_run_violation = {"starting_violation": most_recent_violation}

    return next_run_violation, incidents


def get_human_readable(violation: List[dict[str, Any]]) -> str:
    """Parse results into human readable format

    :type violation: List
    :param violation: List object obtaining violation data

    :return: String with Markdown formatting
    :rtype: str
    """

    def violation_to_str(v):
        return (
            f'### Violation {v["violation_id"]} \r'
            f'|Violation ID|Status|Timestamp|Dashboard URL|User|App Name| \r'
            f'|---|---|---|---|---|---| \r'
            f'| {v["violation_id"]} | {v["violation_status"]} | '
            f'{timestamp_to_datestring(v["violation_event_timestamp"] * 1000)} | '
            f'{v["dashboard_url"]} | {v["user"]} | {v["app_name"]} | \r'
        )

    return "\r".join(violation_to_str(key) for key in violation)


def main() -> None:
    """main function, parses params and runs command functions

    :return:
    :rtype:
    """

    demisto.debug(f"Command being called is {demisto.command()}")
    try:
        client = Client(demisto)

        if demisto.command() == "fetch-incidents":
            str_first_fetch_violation = demisto.params().get("first_fetch", 1)
            str_max_results = demisto.params().get("max_fetch", 10)

            next_run_violation, incidents = fetch_incidents(
                client=client,
                last_run_violation=demisto.getLastRun(),
                str_first_fetch_violation=str_first_fetch_violation,
                str_max_results=str_max_results,
            )

            demisto.setLastRun(next_run_violation)
            demisto.incidents(incidents)
        elif demisto.command() == "test-module":
            result = test_module(client)
            return_results(result)
        else:
            return_results(Command.run(demisto.command(), client, demisto.args()))

    except Exception as e:
        demisto.error(traceback.format_exc())  # print the traceback
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{str(e)}")


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()