Cognni

Autonomous detection and investigation of information security incidents and other potential threats.

Utilities · Cognni

Details

IDCognni
ProviderCognni
CategoryUtilities
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

The Cognni connector offers a quick and simple integration with Cortex XSOAR
in order to provide ongoing insights into how your important information is used.
With Cognni, you can autonomously detect information-specific incidents
based on contextual factors, and automatically compile insights to investigate
how incidents occur. This intelligence provides the details you need to remediate
incidents, fast enough to make a difference.

This integration was integrated and tested with version 1.0 of Cognni

Configure Cognni in Cortex

Parameter Description Required
url Server URL True
isFetch Fetch incidents False
incidentType Incident type False
max_fetch Maximum number of events per fetch False
apikey API Key True
min_severity Minimum severity of alerts to fetch True
first_fetch First fetch time False
insecure Trust any certificate (not secure) False
proxy 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.

cognni-get-event


Fetches a single event by ID.

Base Command

cognni-get-event

Input

Argument Name Description Required
event_id The ID of the event to fetch. Required

Context Output

Path Type Description
Cognni.Event.id String Event ID.
Cognni.Event.date Date The date when the event occurred.
Cognni.Event.description String Description of the event.
Cognni.Event.severity Number Severity of the event.
Cognni.Event.sourceApplication String The ID of the application which initiated the event.

Command Example

!cognni-get-event event_id="9ba7fb56-8ace-4b3d-a1e9-08c466668e57"

Context Example

{
    "Cognni": {
        "event": {
            "id": "9ba7fb56-8ace-4b3d-a1e9-08c466668e57",
            "description": "N/A",
            "sourceApplication": "Exchange",
            "date": "2020-11-25T00:46:14.000Z"
        }
    }
}

Human Readable Output

Cognni event 9ba7fb56-8ace-4b3d-a1e9-08c466668e57

date description id sourceApplication
2020-11-25T00:46:14.000Z N/A 9ba7fb56-8ace-4b3d-a1e9-08c466668e57 Exchange

cognni-get-insight


Fetches a single insight by ID.

Base Command

cognni-get-insight

Input

Argument Name Description Required
insight_id The ID of the insight to fetch. Required

Context Output

Path Type Description
Cognni.insight.id String Insight ID.
Cognni.insight.name String Name of the insight.
Cognni.insight.description String Description of the insight.
Cognni.insight.severity Number Severity of the insight.

Command Example

!cognni-get-insight insight_id="74a53ab3-3e75-4444-9e7c-0be1e1bc26a9"

Context Example

{
    "Cognni": {
        "insights": {
            "id": "c24405d5-49f5-48b8-b15c-1a1aba540979",
            "name": "Medium sensitivity content, Shared to private email address",
            "description": null,
            "severity": 2
        }
    }
}

Human Readable Output

Cognni 1 insight

description id name severity
  c24405d5-49f5-48b8-b15c-1a1aba540979 Medium sensitivity content, Shared to private email address 2

cognni-fetch-insights


Fetches insights according to severity.

Base Command

cognni-fetch-insights

Input

Argument Name Description Required
min_severity Minimum severity of insights to fetch. Default is 2. Required

Context Output

Path Type Description
Cognni.insights.id String List of insight IDs.
Cognni.insights.name String List of insight names.
Cognni.insights.description String List of insight descriptions.
Cognni.insights.severity Number List of insight severities.

Command Example

!cognni-fetch-insights min_severity=2

Context Example

{
    "Cognni": {
        "insights": [
            {
                "description": null,
                "id": "4539ff6d-c58b-4a2a-a509-f121edbe97d7",
                "name": "High sensitive Anonymous share",
                "severity": 3
            },
            {
                "description": null,
                "id": "0875799c-6077-4f5f-b276-0e7baa2b89ab",
                "name": "High sensitive content Shared inside the organization Anomaly",
                "severity": 2
            },
            {
                "description": null,
                "id": "169b10e0-0970-430b-9709-61ccc312fdd0",
                "name": "High Sensitive content Shared Outside the organization Anomaly",
                "severity": 3
            },
            {
                "description": null,
                "id": "4cf8297f-b311-4cfa-9e8e-935606907e5f",
                "name": "High Sensitive content Shared to private email address",
                "severity": 3
            },
            {
                "description": null,
                "id": "537aa700-0eed-4998-b253-f809e1eacc00",
                "name": "High sensitive content Shared to private email Address Anomaly",
                "severity": 3
            },
            {
                "description": null,
                "id": "df061da3-13c1-4a59-8501-4d26bacd5b83",
                "name": "Low Sensitive content Anonymous Share",
                "severity": 2
            },
            {
                "description": null,
                "id": "c7723427-b075-4259-8fbc-19dab3861b92",
                "name": "Low sensitive content Shared to private email address Anomaly",
                "severity": 2
            },
            {
                "description": null,
                "id": "846c753b-1feb-4d21-ae43-ec81b9725636",
                "name": "Medium sensitivity content, Anonymous share",
                "severity": 3
            },
            {
                "description": null,
                "id": "f964659c-9cc3-4833-b535-0402cd953376",
                "name": "Medium sensitivity content Shared outside the organization Anomaly",
                "severity": 2
            },
            {
                "description": null,
                "id": "c24405d5-49f5-48b8-b15c-1a1aba540979",
                "name": "Medium sensitivity content, Shared to private email address",
                "severity": 2
            },
            {
                "description": null,
                "id": "c925372e-c2d5-4b61-b37e-399263ad58f9",
                "name": "Medium sensitivity content Shared to private email Address Anomaly",
                "severity": 3
            }
        ]
    }
}

Human Readable Output

Cognni 11 insights

description id name severity
  4539ff6d-c58b-4a2a-a509-f121edbe97d7 High sensitive Anonymous share 3
  0875799c-6077-4f5f-b276-0e7baa2b89ab High sensitive content Shared inside the organization Anomaly 2
  169b10e0-0970-430b-9709-61ccc312fdd0 High Sensitive content Shared Outside the organization Anomaly 3
  4cf8297f-b311-4cfa-9e8e-935606907e5f High Sensitive content Shared to private email address 3
  537aa700-0eed-4998-b253-f809e1eacc00 High sensitive content Shared to private email Address Anomaly 3
  df061da3-13c1-4a59-8501-4d26bacd5b83 Low Sensitive content Anonymous Share 2
  c7723427-b075-4259-8fbc-19dab3861b92 Low sensitive content Shared to private email address Anomaly 2
  846c753b-1feb-4d21-ae43-ec81b9725636 Medium sensitivity content, Anonymous share 3
  f964659c-9cc3-4833-b535-0402cd953376 Medium sensitivity content Shared outside the organization Anomaly 2
  c24405d5-49f5-48b8-b15c-1a1aba540979 Medium sensitivity content, Shared to private email address 2
  c925372e-c2d5-4b61-b37e-399263ad58f9 Medium sensitivity content Shared to private email Address Anomaly 3

Configuration parameters

  • url — Server URL (required)
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • max_fetch — Maximum number of events per fetch
  • apikey — API Key (required)
  • min_severity — Minimum severity of alerts to fetch (required)
  • first_fetch — First fetch time
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (3)

  • cognni-fetch-insights

    Fetches insights according to severity.

  • cognni-get-event

    Fetches a single event by ID.

  • cognni-get-insight

    Fetches a single insight by ID.

# import demistomock as demisto
import traceback
from collections.abc import Iterable
from datetime import datetime
from typing import Any, cast

import dateparser
import urllib3
from CommonServerPython import *

urllib3.disable_warnings()

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
MAX_EVENTS_TO_FETCH = 50
COGNNI_SEVERITIES = ["Low", "Medium", "High", "Critical"]
SUNDAY_ISO_WEEKDAY = 7

""" CLIENT CLASS """


class Client(BaseClient):
    """Client class to interact with the service API

    This Client implements API calls, and does not contain any Demisto logic.
    Should only do requests and return data.
    It inherits from BaseClient defined in CommonServer Python.
    Most calls use _http_request() that handles proxy, SSL verification, etc.
    """

    def fetch_key(self, api_key: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=f"/api/v1/login/key/{api_key}")

    def graphql(self, query: str, variables: dict[str, Any] | None = None) -> dict[str, Any]:
        if not variables:
            variables = {}

        graphql_operation = {"query": query, "variables": variables}

        res = self._http_request(method="POST", url_suffix="/intelligence/data/graphql", json_data=graphql_operation)
        return res["data"]

    def ping(self) -> dict[str, Any]:
        query = "{ping}"

        return self.graphql(query=query)

    def fetch_events(self, min_severity: int, start_time: str, events_limit: int, offset: int) -> list[dict[str, Any]]:
        query = (
            """
            query($severityValue:String!, $pagination:Pagination) {
              events(
                filter: {
                  coordinates: [
                    {
                      x: {
                        type: None,
                        value: "none"
                      },
                      y: {
                        type: Severity,
                        value: $severityValue
                      },
                      z: {
                        type:Week,
                        values:[\""""
            + start_time
            + """\"]
                      }
                    }
                ]
                  pagination: $pagination
                }
            ) {
                eventId: id
                description
                severity
                sourceApplication
                date
                items {
                    itemId: id
                    externalId
                    type
                    name
                    clusterUID
                    data
                    createdAt
                    labels {
                        name
                    }
                }
                insights {
                    name
                }
            }
        }
        """
        )

        variables = {
            "pagination": {"limit": events_limit, "offset": offset, "direction": "Ascend"},
            "severityValue": str(min_severity),
        }
        res = self.graphql(query=query, variables=variables)
        return res["events"]

    def get_event(self, event_id: str) -> dict[str, Any]:
        query = """
            query ($event_id: ID!) {
                event(id: $event_id){
                    id
                    description
                    sourceApplication
                    date
                }
            }
        """

        variables = {"event_id": event_id}

        res = self.graphql(query=query, variables=variables)
        return res["event"]

    def fetch_insights(self, min_severity: int) -> list[dict[str, Any]]:
        query = """
             query ($min_severity: Int) {
                 insights(minSeverity: $min_severity){
                     id
                     description
                     name
                     severity
                 }
             }
         """

        variables = {"min_severity": int(min_severity)}

        res = self.graphql(query=query, variables=variables)
        return res["insights"]

    def get_insight(self, insight_id: str) -> dict[str, Any]:
        query = """
            query ($insight_id: ID!) {
              insight(id: $insight_id) {
                  id
                  name
                  description
                  severity
              }
            }
        """

        variables = {"insight_id": insight_id}

        res = self.graphql(query=query, variables=variables)
        return res["insight"]


""" HELPER FUNCTIONS """


def convert_to_demisto_severity(severity: str) -> int:
    """Maps Cognni severity to Cortex XSOAR severity

    Converts the Cognni alert severity level ('Low', 'Medium',
    'High', 'Critical') to Cortex XSOAR incident severity (1 to 4)
    for mapping.

    :type severity: ``str``
    :param severity: severity as returned from the Cognni API (str)

    :return: Cortex XSOAR Severity (1 to 4)
    :rtype: ``int``
    """
    return {
        "Low": 1,  # low severity
        "Medium": 2,  # medium severity
        "High": 3,  # high severity
        "Critical": 4,  # critical severity
    }[severity]


def convert_to_demisto_severity_int(severity: int) -> int:
    """Maps Cognni severity to Cortex XSOAR severity

    Converts the Cognni alert severity level ('Low', 'Medium',
    'High', 'Critical') to Cortex XSOAR incident severity (1 to 4)
    for mapping.

    :type severity: ``str``
    :param severity: severity as returned from the Cognni API (str)

    :return: Cortex XSOAR Severity (1 to 4)
    :rtype: ``int``
    """

    return severity


def arg_to_int(arg: Any, arg_name: str, required: bool = False) -> int | None:
    """Converts an XSOAR argument to a Python int

    This function is used to quickly validate an argument provided to XSOAR
    via ``demisto.args()`` into an ``int`` type. It will throw a ValueError
    if the input is invalid. If the input is None, it will throw a ValueError
    if required is ``True``, or ``None`` if required is ``False.

    :type arg: ``Any``
    :param arg: argument to convert

    :type arg_name: ``str``
    :param arg_name: argument name

    :type required: ``bool``
    :param required:
        throws exception if ``True`` and argument provided is None

    :return:
        returns an ``int`` if arg can be converted
        returns ``None`` if arg is ``None`` and required is set to ``False``
        otherwise throws an Exception
    :rtype: ``Optional[int]``
    """

    if arg is None:
        if required is True:
            raise ValueError(f'Missing "{arg_name}"')
        return None
    if isinstance(arg, str):
        if arg.isdigit():
            return int(arg)
        raise ValueError(f'Invalid number: "{arg_name}"="{arg}"')
    if isinstance(arg, int):
        return arg
    raise ValueError(f'Invalid number: "{arg_name}"')


def arg_to_timestamp(arg: Any, arg_name: str, required: bool = False) -> int | None:
    """Converts an XSOAR argument to a timestamp (seconds from epoch)

    This function is used to quickly validate an argument provided to XSOAR
    via ``demisto.args()`` into an ``int`` containing a timestamp (seconds
    since epoch). It will throw a ValueError if the input is invalid.
    If the input is None, it will throw a ValueError if required is ``True``,
    or ``None`` if required is ``False.

    :type arg: ``Any``
    :param arg: argument to convert

    :type arg_name: ``str``
    :param arg_name: argument name

    :type required: ``bool``
    :param required:
        throws exception if ``True`` and argument provided is None

    :return:
        returns an ``int`` containing a timestamp (seconds from epoch) if conversion works
        returns ``None`` if arg is ``None`` and required is set to ``False``
        otherwise throws an Exception
    :rtype: ``Optional[int]``
    """

    if arg is None:
        if required is True:
            raise ValueError(f'Missing "{arg_name}"')
        return None

    if isinstance(arg, str) and arg.isdigit():
        return int(arg)
    if isinstance(arg, str):
        date = dateparser.parse(arg, settings={"TIMEZONE": "UTC"})
        if date is None:
            raise ValueError(f"Invalid date: {arg_name}")

        return int(date.timestamp())
    if isinstance(arg, int | float):
        return int(arg)
    raise ValueError(f'Invalid date: "{arg_name}"')


def flatten_event_file_items(event: dict[str, Any]):
    if not event or not event["items"]:
        return []
    return [
        {
            "eventId": event.get("eventId"),
            "fileName": item.get("name"),
            "fileId": item.get("itemId"),
            "name": item.get("name"),
            "eventType": item.get("type"),
            "description": event.get("description"),
            "date": event.get("date"),
            "severity": event.get("severity"),
            "sourceApplication": event.get("sourceApplication"),
        }
        for item in event["items"]
    ]


def convert_file_event_to_incident(file_event: dict[str, Any]):
    return {
        "name": file_event.get("name"),
        "details": file_event["description"],
        "occurred": file_event.get("date"),
        "rawJSON": json.dumps(file_event),
        "severity": convert_to_demisto_severity_int(file_event.get("severity", 1)),
    }


def convert_events_to_incidents(events: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
    if not events:
        return []

    file_events: list[dict[str, Any]] = sum(map(flatten_event_file_items, events), [])

    incidents = list(map(convert_file_event_to_incident, file_events))

    return incidents


def find_latest_event(events: Iterable[dict[str, Any]]) -> dict[str, Any] | None:
    last_date = 0
    latest_event = None

    for event in events:
        event_date = date_to_timestamp(date_str_or_dt=event.get("date", ""), date_format="%Y-%m-%dT%H:%M:%S.000Z")
        if last_date < event_date:
            last_date = event_date
            latest_event = event

    return latest_event


""" COMMAND FUNCTIONS """


def test_module(client: Client, api_key: str, first_fetch: int) -> str:
    """Tests API connectivity and authentication'

    Returning 'ok' indicates that the integration works like it is supposed to.
    Connection to the service is successful.
    Raises exceptions if something goes wrong.

    :type client: ``Client``
    :param client: Cognni client to use

    :return: 'ok' if test passed, anything else will fail the test.
    :rtype: ``str``
    """
    answer = ""
    try:
        client.fetch_key(api_key)
    except ValueError:
        answer += "The api key is invalid"
    try:
        timestamp_to_datestring(timestamp=first_fetch * 1000, date_format="%Y-%m-%d")
    except ValueError:
        answer += "Incorrect first fetch time format, should be YYYY-MM-DD"

    if not answer:
        return "ok"
    else:
        return answer


def fetch_incidents(
    client: Client, last_run: dict[str, int], first_fetch_time: int | None, events_limit: int, min_severity: int
) -> tuple[dict[str, int], list[dict]]:
    """This function retrieves new alerts every interval (default is 1 minute).

    This function has to implement the logic of making sure that incidents are
    fetched only once and no incidents are missed. By default it's invoked by
    XSOAR every minute. It will use last_run to save the timestamp of the last
    incident it processed. If last_run is not provided, it should use the
    integration parameter first_fetch_time to determine when to start fetching
    the first time.

    :param events_limit:
    :type client: ``Client``
    :param client: Cognni client to use

    :type last_run: ``Optional[Dict[str, int]]``
    :param last_run:
        A dict with a key containing the latest incident created time we got
        from last fetch

    :type first_fetch_time: ``Optional[int]``
    :param first_fetch_time:
        If last_run is None (first time we are fetching), it contains
        the timestamp in milliseconds on when to start fetching incidents

    :type min_severity: ``str``
    :param min_severity:
        minimum severity of the alert to search for.
        Options are: "Low", "Medium", "High", "Critical"

    :return:
        A tuple containing two elements:
            next_run (``Dict[str, int]``): Contains the timestamp that will be
                    used in ``last_run`` on the next fetch.
            incidents (``List[dict]``): List of incidents that will be created in XSOAR

    :rtype: ``Tuple[Dict[str, int], List[dict]]``
    """

    last_fetch = last_run.get("last_fetch", None)
    is_initial_run = last_run.get("is_initial_run", True)
    offset = last_run.get("offset")
    if last_fetch is None:
        last_fetch = first_fetch_time
    else:
        last_fetch = int(last_fetch)

    latest_created_time = cast(int, last_fetch)

    if offset is None or (
        not is_initial_run
        and datetime.utcnow().isoweekday() == SUNDAY_ISO_WEEKDAY
        and datetime.utcfromtimestamp(latest_created_time).isoweekday() != SUNDAY_ISO_WEEKDAY
    ):
        offset = 0

    events = client.fetch_events(
        events_limit=events_limit,
        offset=offset,
        start_time=timestamp_to_datestring(timestamp=latest_created_time * 1000, is_utc=True),
        min_severity=min_severity,
    )

    if not events:
        next_run = {"last_fetch": latest_created_time, "offset": offset, "is_initial_run": False}
        return next_run, []

    latest_event = find_latest_event(events)
    if latest_event:
        latest_created_time = int(
            date_to_timestamp(date_str_or_dt=latest_event.get("date", latest_created_time), date_format="%Y-%m-%dT%H:%M:%S.000Z")
            / 1000
        )

    incidents = convert_events_to_incidents(events)

    next_run = {"last_fetch": latest_created_time, "offset": offset + len(events), "is_initial_run": is_initial_run}

    return next_run, incidents


def get_event_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """cognni-get-event command: Returns a Cognni event

    :type client: ``Client``
    :param client: Cognni client to use

    :type args: ``Dict[str, Any]``
    :param args:
        all command arguments, usually passed from ``demisto.args()``.
        ``args['event_id']`` alert ID to return

    :return:
        A ``CommandResults`` object that is then passed to ``return_results``,
        that contains an alert

    :rtype: ``CommandResults``
    """

    event_id = args.get("event_id", None)
    if not event_id:
        raise ValueError("event_id not specified")

    event = client.get_event(event_id=event_id)
    readable_output = tableToMarkdown(f"Cognni event {event_id}", event)

    return CommandResults(readable_output=readable_output, outputs_prefix="Cognni.event", outputs_key_field="id", outputs=event)


def fetch_insights_command(client: Client, args: dict[str, Any]) -> CommandResults:
    min_severity = int(args.get("min_severity", 2))

    insights = client.fetch_insights(min_severity=min_severity)

    readable_output = tableToMarkdown(f"Cognni {len(insights)} insights", insights)

    return CommandResults(
        readable_output=readable_output, outputs_prefix="Cognni.insights", outputs_key_field="id", outputs=insights
    )


def get_insight_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """cognni-get-insight command: Returns a Cognni event

    :type client: ``Client``
    :param client: Cognni client to use

    :type args: ``Dict[str, Any]``
    :param args:
        all command arguments, usually passed from ``demisto.args()``.
        ``args['event_id']`` alert ID to return

    :return:
        A ``CommandResults`` object that is then passed to ``return_results``,
        that contains an alert

    :rtype: ``CommandResults``
    """

    insight_id = args.get("insight_id", None)
    if not insight_id:
        raise ValueError("insight_id not specified")

    insight = client.get_insight(insight_id=insight_id)

    readable_output = tableToMarkdown(f"Cognni event {insight_id}", insight)

    return CommandResults(
        readable_output=readable_output, outputs_prefix="Cognni.insight", outputs_key_field="id", outputs=insight
    )


""" MAIN FUNCTION """


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

    :return:
    :rtype:
    """

    api_key = demisto.params().get("apikey")

    # get the service API url
    base_url = demisto.params()["url"]

    verify_certificate = not demisto.params().get("insecure", False)

    first_fetch_time = arg_to_timestamp(
        arg=demisto.params().get("first_fetch", "3 days"), arg_name="First fetch time", required=True
    )
    assert isinstance(first_fetch_time, int)
    proxy = demisto.params().get("proxy", False)

    demisto.debug(f"Command being called is {demisto.command()}")
    try:
        client = Client(base_url=base_url, verify=verify_certificate, proxy=proxy)

        if demisto.command() == "test-module":
            # This is the call made when pressing the integration Test button.
            result = test_module(client, api_key, first_fetch_time)
            return_results(result)

        else:
            fetch_key_res = client.fetch_key(api_key)
            access_token = fetch_key_res["token"]

            headers = {
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            }
            client = Client(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy)

            if demisto.command() == "fetch-incidents":
                min_severity = demisto.params().get("min_severity", None)

                max_fetch = arg_to_int(arg=demisto.params().get("max_fetch"), arg_name="max_fetch", required=False)
                if not max_fetch or max_fetch > MAX_EVENTS_TO_FETCH:
                    max_fetch = MAX_EVENTS_TO_FETCH

                next_run, incidents = fetch_incidents(
                    client=client,
                    events_limit=max_fetch,
                    last_run=demisto.getLastRun(),  # getLastRun() gets the last run dict
                    first_fetch_time=first_fetch_time,
                    min_severity=convert_to_demisto_severity(min_severity),
                )
                demisto.setLastRun(next_run)
                demisto.incidents(incidents)

            elif demisto.command() == "cognni-get-event":
                return_results(get_event_command(client, demisto.args()))

            elif demisto.command() == "cognni-fetch-insights":
                return_results(fetch_insights_command(client, demisto.args()))

            elif demisto.command() == "cognni-get-insight":
                return_results(get_insight_command(client, demisto.args()))

    # Log exceptions and return errors
    except Exception as e:
        demisto.error(traceback.format_exc())
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}")


""" ENTRY POINT """

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