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 json
import pytest
import demistomock as demisto
from Gamma import Client, fetch_incidents, Command, main

MOCK_URL = "mock://fake-api.net"

MOCK_VIOLATION = {
    "response": [
        {
            "app_name": "jira",
            "dashboard_url": f"{MOCK_URL}/violationId/2036",
            "file_labels_map": {},
            "text_labels": [],
            "user": {
                "active_directory_user_id": None,
                "atlassian_account_id": None,
                "email_address": None,
                "github_handle": None,
                "name": "Amane Suzuha",
                "slack_user_id": None,
            },
            "violation_category": "mock_category",
            "violation_event_timestamp": 1605805555,
            "violation_id": 2036,
            "violation_status": "OPEN",
        }
    ]
}

MOCK_VIOLATION_2 = {
    "response": [
        {
            "app_name": "jira",
            "dashboard_url": f"{MOCK_URL}/violationId/5100",
            "file_labels_map": {},
            "text_labels": [],
            "user": {
                "active_directory_user_id": None,
                "atlassian_account_id": None,
                "email_address": None,
                "github_handle": None,
                "name": "Rintaro Okabe",
                "slack_user_id": None,
            },
            "violation_category": "mock_category",
            "violation_event_timestamp": 1605804455,
            "violation_id": 5100,
            "violation_status": "OPEN",
        }
    ]
}

MOCK_VIOLATION_2_UPDATED = {
    "response": [
        {
            "app_name": "jira",
            "dashboard_url": f"{MOCK_URL}/violationId/5100",
            "file_labels_map": {},
            "text_labels": [],
            "user": {
                "active_directory_user_id": None,
                "atlassian_account_id": None,
                "email_address": None,
                "github_handle": None,
                "name": "Rintaro Okabe",
                "slack_user_id": None,
            },
            "violation_category": "mock_category",
            "violation_event_timestamp": 1605804455,
            "violation_id": 5100,
            "violation_status": "RESOLVED",
        }
    ]
}

MOCK_ALL_VIOLATIONS = {"response": [MOCK_VIOLATION["response"][0], MOCK_VIOLATION_2["response"][0]]}


def mock_demisto(mocker, args_value=None, command_value=None):
    mocker.patch.object(demisto, "results")
    mocker.patch.object(demisto, "params", return_value={"api_key": "thisisatestkey", "url": MOCK_URL})
    if not args_value:
        args_value = {"entryID": "entry_id", "parseAll": "yes", "codec": "utf-8"}
    if command_value:
        mocker.patch.object(demisto, "command", return_value=command_value)
    mocker.patch.object(demisto, "args", return_value=args_value)


def mock_client(mocker, demisto):
    mocker.patch.object(demisto, "params", return_value={"api_key": "thisisatestkey", "url": MOCK_URL})

    client = Client(demisto)

    return client


@pytest.mark.parametrize(
    "last_run_violation,first_fetch_violation,max_results,output_1,output_2",
    [
        ({}, "1", "10", "Gamma Violation 2036", 5100),
        ({}, "1", "0", "Gamma Violation 2036", 5100),
        ({}, "1", "-1", "Gamma Violation 2036", 5100),
        ({}, "1", "200", "Gamma Violation 2036", 5100),
    ],
)
def test_fetch_incidents(last_run_violation, first_fetch_violation, max_results, output_1, output_2, requests_mock, mocker):
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_ALL_VIOLATIONS)

    # Test fetch
    next_run, incidents = fetch_incidents(mock_client(mocker, demisto), last_run_violation, first_fetch_violation, max_results)
    mocker.patch.object(demisto, "incidents", incidents)
    assert output_1 == demisto.incidents[0]["name"]
    assert output_2 == next_run["starting_violation"]


@pytest.mark.parametrize(
    "next_run,first_fetch_violation,max_results,output_1,output_2",
    [({"starting_violation": 2036}, "1", "10", 1, 5100), ({"starting_violation": 5100}, "1", "10", 0, 5100)],
)
def test_fetch_incidents_next_fetch(next_run, first_fetch_violation, max_results, output_1, output_2, requests_mock, mocker):
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_ALL_VIOLATIONS)

    next_run, incidents = fetch_incidents(mock_client(mocker, demisto), next_run, first_fetch_violation, max_results)

    assert output_1 == len(incidents)
    assert output_2 == next_run["starting_violation"]


@pytest.mark.parametrize(
    "last_run_violation,first_fetch_violation,max_results,output",
    [
        ({}, "0", "10", "first_fetch_violation must be equal to 1 or higher"),
        ({}, "-1", "10", "first_fetch_violation must be equal to 1 or higher"),
        ({}, "test", "10", "first_fetch_violation and max_limit must be integers"),
        ({}, "1", "test", "first_fetch_violation and max_limit must be integers"),
    ],
)
def test_fetch_incidents_bad_input(last_run_violation, first_fetch_violation, max_results, output, mocker):
    with pytest.raises(ValueError) as err:
        fetch_incidents(mock_client(mocker, demisto), last_run_violation, first_fetch_violation, max_results)
    assert output == str(err.value)


def test_get_violation_command(requests_mock, mocker, capfd):
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_VIOLATION)

    args = {"violation": "2036"}
    mock_demisto(mocker, args, "gamma-get-violation")
    with capfd.disabled():
        main()
    content = demisto.results.call_args[0][0]["Contents"][0]["violation_id"]
    assert content == 2036


@pytest.mark.parametrize(
    "demisto_args,output",
    [
        ({"violation": "0"}, "Violation must be greater than 0"),
        ({"violation": "-1"}, "Violation must be greater than 0"),
        ({"violation": "test"}, "invalid literal for int() with base 10: 'test'"),
    ],
)
def test_get_violation_command_bad_input(demisto_args, output, mocker):
    client = mock_client(mocker, demisto)
    with pytest.raises(ValueError) as err:
        Command.get_violation(client, demisto_args)
    assert output == str(err.value)


@pytest.mark.parametrize(
    "demisto_args,output",
    [
        ({"minimum_violation": "2036", "limit": "2"}, 2036),
        ({"minimum_violation": "2035", "limit": "2"}, 2036),
    ],
)
def test_get_violation_list_command(demisto_args, output, requests_mock, mocker, capfd):
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_ALL_VIOLATIONS)

    mock_demisto(mocker, demisto_args, "gamma-get-violation-list")
    with capfd.disabled():
        main()
    content = demisto.results.call_args[0][0]["Contents"][0]["violation_id"]
    assert output == content


@pytest.mark.parametrize(
    "demisto_args,output",
    [
        ({"minimum_violation": "0", "limit": "2"}, "minimum_violation must be greater than 0"),
        ({"minimum_violation": "test", "limit": "2"}, "invalid literal for int() with base 10: 'test'"),
        ({"minimum_violation": "-1", "limit": "2"}, "minimum_violation must be greater than 0"),
        ({"minimum_violation": "2035", "limit": "0"}, "limit must be between 1 and 100"),
        ({"minimum_violation": "2035", "limit": "-1"}, "limit must be between 1 and 100"),
        ({"minimum_violation": "2035", "limit": "test"}, "invalid literal for int() with base 10: 'test'"),
    ],
)
def test_get_violation_list_command_bad_input(demisto_args, output, mocker):
    client = mock_client(mocker, demisto)
    with pytest.raises(ValueError) as err:
        Command.get_violation_list(client, demisto_args)
    assert output == str(err.value)


@pytest.mark.parametrize(
    "demisto_args,demisto_command,output",
    [
        (
            {"violation": "5100", "status": "resolved", "notes": "This has been updated!"},
            "gamma-update-violation",
            "RESOLVED",
        ),
    ],
)
def test_update_violation_command(demisto_args, demisto_command, output, requests_mock, mocker, capfd):
    test_violation = 5100
    requests_mock.put(MOCK_URL + f"/api/discovery/v1/violation/{test_violation}", json=MOCK_VIOLATION_2)
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_VIOLATION_2_UPDATED)

    mock_demisto(mocker, demisto_args, demisto_command)
    with capfd.disabled():
        main()
    contents = demisto.results.call_args[0][0]["Contents"][0]["violation_status"]
    assert output == contents


@pytest.mark.parametrize(
    "demisto_args,output",
    [
        ({"violation": "0", "status": "resolved", "notes": "This has been updated!"}, "Violation must be greater than 0"),
        ({"violation": "-1", "status": "resolved", "notes": "This has been updated!"}, "Violation must be greater than 0"),
        (
            {"violation": "test", "status": "resolved", "notes": "This has been updated!"},
            "invalid literal for int() with base 10: 'test'",
        ),
        (
            {"violation": "5100", "status": "closed", "notes": "This has been updated!"},
            "Status must be one of the following: OPEN, RESOLVED, IGNORED",
        ),
    ],
)
def test_update_violation_command_bad_input(demisto_args, output, mocker):
    client = mock_client(mocker, demisto)
    with pytest.raises(ValueError) as err:
        Command.update_violation(client, demisto_args)
    assert output == str(err.value)


@pytest.mark.parametrize(
    "demisto_args,demisto_command,output_1,output_2",
    [
        ({}, "fetch-incidents", "Gamma Violation 2036", "Gamma Violation 5100"),
        (
            {"first_fetch_violation": "2036", "max_results": "5"},
            "fetch-incidents",
            "Gamma Violation 2036",
            "Gamma Violation 5100",
        ),
    ],
)
def test_main_fetch_incidents(demisto_args, demisto_command, output_1, output_2, requests_mock, mocker, capfd):
    # Test fetch
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_ALL_VIOLATIONS)
    mock_demisto(mocker, demisto_args, demisto_command)
    with capfd.disabled():
        main()
    contents = json.loads(demisto.results.call_args[0][0]["Contents"])
    assert output_1 == contents[0]["name"]
    assert output_2 == contents[1]["name"]


def test_main_get_violation_list(requests_mock, mocker, capfd):
    # Test fetch
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_ALL_VIOLATIONS)
    args = {"minimum_id": "2036", "limit": "5"}
    command = "gamma-get-violation-list"
    mock_demisto(mocker, args, command)
    with capfd.disabled():
        main()
    response = demisto.results.call_args[0][0]["Contents"]
    assert {2036, 5100} == {i["violation_id"] for i in response}


def test_main_get_bad_violation(mocker, requests_mock, capfd):
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_VIOLATION)

    # Test wrong ID
    command = "gamma-get-violation"
    args = {"violation": "5100"}
    mock_demisto(mocker, args, command)
    with pytest.raises(SystemExit), capfd.disabled():
        main()

    assert (
        demisto.results.call_args[0][0]["Contents"] == "Failed to execute gamma-get-violation "
        "command.\nError:\nViolation with this "
        "ID does not exist."
    )


def test_main_get_violation(requests_mock, mocker, capfd):
    # Test get violation
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_VIOLATION)
    args = {"violation": "2036"}
    command = "gamma-get-violation"
    mock_demisto(mocker, args, command)
    with capfd.disabled():
        main()
    assert demisto.results.call_args[0][0]["Contents"][0]["violation_id"] == 2036


def test_main_update(requests_mock, mocker, capfd):
    # Test get violation
    test_violation = 2036
    requests_mock.put(MOCK_URL + f"/api/discovery/v1/violation/{test_violation}", json=MOCK_VIOLATION)
    requests_mock.get(MOCK_URL + "/api/discovery/v1/violation/list", json=MOCK_VIOLATION)
    args = {"violation": f"{test_violation}", "status": "RESOLVED", "notes": ""}
    command = "gamma-update-violation"
    mock_demisto(mocker, args, command)
    with capfd.disabled():
        main()

    assert test_violation == demisto.results.call_args[0][0]["Contents"][0]["violation_id"]


def test_bad_command(mocker, capfd):
    test_violation = 2036
    args = {"violation": f"{test_violation}", "status": "resolved", "notes": ""}
    command = "gamma-violation-update"
    mock_demisto(mocker, args, command)
    with pytest.raises(SystemExit), capfd.disabled():
        main()
    assert (
        demisto.results.call_args[0][0]["Contents"] == "Failed to execute "
        "gamma-violation-update "
        "command.\nError:\nCommand "
        '"gamma-violation-update" is not '
        "implemented."
    )