Stealthwatch Cloud

Protect your cloud assets and private network.

Network Security · Cisco Secure Cloud Analytics (Stealthwatch Cloud)

Details

IDStealthwatch Cloud
ProviderCisco Systems
CategoryNetwork Security
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10325753
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security

README

Overview


Use the Cisco Secure Cloud Analytics (Stealthwatch Cloud) integration to manage threats to your networks.

This integration was integrated and tested with Cisco Secure Cloud Analytics (Stealthwatch Cloud) v1.0.0.

Use cases


  1. Fetch incidents
  2. Block domains (Block list)
  3. Update alerts

Configure Cisco Secure Cloud Analytics (Stealthwatch Cloud) on Cortex XSOAR


  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Stealthwatch Cloud.
  3. Click Add instance to create and configure a new integration instance.
    • Name: a textual name for the integration instance.
    • Stealthwatch server URL
    • Stealthwatch Cloud API key. Should be in the form of "ApiKey :<api_key>"
    • Use system proxy settings
    • Trust any certificate (not secure)
    • Fetch incidents
    • Incident type
  4. 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.

  1. Get information for an alert: sw-show-alert
  2. Update an alert: sw-update-alert
  3. Get a list of all alerts: sw-list-alerts
  4. Block a domain or IP: sw-block-domain-or-ip
  5. Unblock a domain: sw-unblock-domain
  6. Get a list of blocked domains: sw-list-blocked-domains
  7. Get a list of observations: sw-list-observations
  8. Get a list of sessions by session occurrence time: sw-list-sessions

1. Get information for an alert


Returns information about a specific alert by the alert ID.

Base Command
sw-show-alert
Input
Argument Name Description Required
alertID The id of the required alert Required
addComments Add comments information, can be long Optional
 
Context Output
Path Type Description
Stealthwatch.Alert.id number Alert ID
Stealthwatch.Alert.assigned_to string Alert assignee
Stealthwatch.Alert.obj_created date Alert creation date
Stealthwatch.Alert.last_modified date Alert last modification
Stealthwatch.Alert.resolved boolean Alert state
Stealthwatch.Alert.source_info.ips string IP of the alert's source
Stealthwatch.Alert.source_info.hostnames string Hostname of the alert's source
 
Command Example
!sw-show-alert alertID=275
Context Example
{
  "assigned_to": null,
  "assigned_to_username": null,
  "created": "2018-07-23T15:30:00Z",
  "description": "Source has started a port scan on a device internal to your network.",
  "hostname": "",
  "id": 275,
  "ips_when_created": [],
  "last_modified": "2018-10-02T22:41:07.749868Z",
  "merit": 3,
  "natural_time": "1 month ago",
  "obj_created": "2018-07-23T16:34:01.566717Z",
  "priority": 2,
  "publish_time": "2018-07-23T16:34:01.531458+00:00",
  "resolved": true,
  "resolved_time": "2018-11-17T05:00:01.458445Z",
  "resolved_user": null,
  "rules_matched": null,
  "snooze_settings": null,
  "source": 48858,
  "source_info": {
    "created": "2018-09-23T15:49:39.025415+00:00",
    "hostnames": [],
    "ips": [
      "5.5.255.25"
    ],
    "name": "test.com",
    "namespace": "default"
  },
  "source_name": "5.5.255.25",
  "source_params": {
    "id": 48852,
    "meta": "net-link",
    "name": "test.com"
  },
  "tags": [],
  "text": "Internal Port Scanner on 5.5.255.25",
  "time": "2018-10-02T21:49:00Z",
  "type": "Internal Port Scanner"
}

2. Update an alert


Updates an alert.

Base Command
sw-update-alert
Input
Argument Name Description Required
alertID The ID of the alert to update Required
resolved Set the resolved field to true and set the merit field to close an alert. merit can be 8 ("helpful") or 9 ("not helpful") Optional
merit Set the resolved field to true and set the merit field to close an alert. merit can be 8 ("helpful") or 9 ("not helpful") Optional
tags Tags (string) Optional
new_comment Set the new_comment field to add a comment to the alert Optional
publish_time Publish time (string), e.g., publish_time=2018-08-01T07:54:39Z Optional
snooze_settings Snooze settings (string) Optional
resolved_user Username (string) Optional
assigned_to Assigned to (integer) Optional
 
Context Output
Path Type Description
Stealthwatch.Alert.id number Alert ID
Stealthwatch.Alert.assigned_to string Alert assignee
Stealthwatch.Alert.obj_created date Alert creation date
Stealthwatch.Alert.last_modified date Date the alert was last modified
Stealthwatch.Alert.resolved boolean Alert state
Stealthwatch.Alert.source_info.ips string IP of the alert's source
Stealthwatch.Alert.source_info.hostname string Hostname of the alert's source
 
Command Example
!sw-update-alert alertID=275 merit=8 tags=test

3. Get a list of all alerts


Get the list of Stealthwatch alerts.

Base Command
sw-list-alerts
Input
Argument Name Description Required
status Filters alerts by status: open, closed, or all. Default is open. The all status enables you to see an individual alert whether it is open or closed. Optional
search Finds a particular string in the alerts, e.g., a particular IP address, hostname, or alert type. Optional
assignee Filter to only display alerts assigned to a specific user Optional
tags Tags shows alerts that are assigned a particular incident tag Optional
limit Number of alerts to list, default is 5 Optional
addComments Add comment to an alert, long-text supported Optional
 
Context Output
Path Type Description
Stealthwatch.Alert.id number Alert ID
Stealthwatch.Alert.assigned_to string Alert assignee
Stealthwatch.Alert.obj_created date Alert creation date
Stealthwatch.Alert.last_modified date Date the alert was last modified
Stealthwatch.Alert.resolved boolean Alert state
Stealthwatch.Alert.source_info.ips string IP of the alert's source
Stealthwatch.Alert.source_info.hostname string Hostname of the alert's source
 
Command Example
{
  "assigned_to": null,
  "assigned_to_username": null,
  "created": "2018-07-23T15:30:00Z",
  "description": "Source has started a port scan on a device internal to your network.",
  "hostname": "",
  "id": 275,
  "ips_when_created": [],
  "last_modified": "2018-10-02T22:41:07.749868Z",
  "merit": 3,
  "natural_time": "1 month ago",
  "obj_created": "2018-07-23T16:34:01.566717Z",
  "priority": 2,
  "publish_time": "2018-07-23T16:34:01.531458+00:00",
  "resolved": true,
  "resolved_time": "2018-11-17T05:00:01.458445Z",
  "resolved_user": null,
  "rules_matched": null,
  "snooze_settings": null,
  "source": 48858,
  "source_info": {
    "created": "2018-09-23T15:49:39.025415+00:00",
    "hostnames": [],
    "ips": [
      "5.5.255.25"
    ],
    "name": "test.com",
    "namespace": "default"
  },
  "source_name": "5.5.255.25",
  "source_params": {
    "id": 48852,
    "meta": "net-link",
    "name": "test.com"
  },
  "tags": [],
  "text": "Internal Port Scanner on 5.5.255.25",
  "time": "2018-10-02T21:49:00Z",
  "type": "Internal Port Scanner"
}

4. Block a domain or IP


Adds a domain or IP to the block list.

Base Command
sw-block-domain-or-ip
Input
Argument Name Description Required
domain Domain to add to the block list Optional
ip IP to add to the block list Optional
 
Context Output
Path Type Description
Stealthwatch.Domain.identifier string Domain name
Stealthwatch.Domain.title string Domain title
Stealthwatch.Domain.id number Domain ID
Stealthwatch.IP.identifier string IP address
Stealthwatch.IP.title string IP title
Stealthwatch.IP.id string IP ID
 
Command Example
!sw-block-domain-or-ip domain=test.com

5. Unblock a domain


Removes a domain from the block list.

Base Command
sw-unblock-domain
Input
Argument Name Description Required
id ID of the domain to remove from the block list. You can find the id by running the sw-list-blocked-domains command. True
 
Context Output

There is no context output for this command.

Command Example
!sw-unblock-domain id=5

6. Get a list of blocked domains


Returns a list of blocked domains.

Base Command
sw-list-blocked-domains
Input
Argument Name Description Required
search Finds a particular string in the alerts, e.g., a particular IP address, hostname, or alert type. Optional
domain Search for a specific domain Optional
limit Number of domains to list, default is 5 Optional
 
Context Output
Path Type Description
Stealthwatch.Domain.identifier string Domain name
Stealthwatch.Domain.title string Domain title
Stealthwatch.Domain.id number Domain ID
 
Command Example
!sw-list-blocked-domains limit=5

7. Get a list of observations


Returns observations by alert ID, observation ID, or a free search.

Base Command
sw-list-observations
Input
Argument Name Description Required
search Finds a particular string amongst the alerts. For example, a particular IP address, hostname, or alert type. False
alert Use the alert query parameter with an alert id to only show observations referenced by the alert False
id Get a specific observation by its ID False
limit Amount of observations to list. Default is 5 False
 
Context Output
Path Type Description
Stealthwatch.Observation.id number Observation ID
Stealthwatch.Observation.port_count number Observation port count
Stealthwatch.Observation.creation_time string Observation creation time
Stealthwatch.Observation.end_time string Observation end time
Stealthwatch.Observation.scanned_ip string Observation scanned ip
Stealthwatch.Observation.scanner_ip string Observation scanner ip
Stealthwatch.Observation.source unknown Observation source
 
Command Example
!sw-list-observations alert=222
Context Example
{
 "cidr_range": "5.5.5.179/32",
 "connected_ip": null,
 "connected_ip_country_code": "",
 "creation_time": "2018-07-23T15:30:00Z",
 "end_time": "2018-07-23T15:30:00Z",
 "id": 12345,
 "observation_name": "Port Scanner",
 "port_count": 24,
 "port_ranges": "0-1023",
 "resource_name": "port_scanner_v1",
 "scan_type": "internal",
 "scanned_packets": 5,
 "scanner_packets": 75,
 "source": 48822,
 "time": "2018-07-23T15:30:00Z"
}

8. Get a list of sessions by session occurrence time


Get sessions by the session's occurrence time ( Time format: YYYY-MM-DDTHH:MM:SSZ)

Base Command
sw-list-sessions
Input
Argument Name Description Required
startTime Session start time (UTC), e.g., startTime="2018-09-30T12:00:00Z" Required
endTime Session end time (UTC), e.g., endTime="2018-07-31T15:00:00Z" Optional
limit Number of observations to list, default is 400 Optional
ip Source IP address to filter by Optional
connectedIP Connected IP to filter by Optional
connectedDeviceId Connected device ID Optional
sessionType Type of session - select external/internal to receive data only about this type of session Optional
 
Context Output
Path Type Description
Stealthwatch.Session.id number Session ID
Stealthwatch.Session.port number Session port
Stealthwatch.Session.start_timestamp_utc string Session start time
Stealthwatch.Session.ip string Session IP
Stealthwatch.Session.connected_ip string Session connected IP
Stealthwatch.Session.device_id number Source device ID
Stealthwatch.Session.connected_device_id number Connected device ID
Stealthwatch.Session.connected_device_is_external boolean Is the connected device external
 
Command Example
!sw-list-sessions startTime="2018-10-30T12:00:00Z" endTime="2018-11-01T12:00:00Z"

Configuration parameters

  • serverURL — Stealthwatch's server URL (required)
  • APIKey — Stealthwatch Cloud API key. Should be in the form of "ApiKey <username>:<apikey>"
  • credentials_api_key
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (8)

  • sw-block-domain-or-ip

    Add a domain or IP to the block list

  • sw-list-alerts

    Get the list of Stealthwatch alerts

  • sw-list-blocked-domains

    Returns list of the blocked domains

  • sw-list-observations

    Get observations by alert ID, observation ID, or free search

  • sw-list-sessions

    Get sessions by the session's occurrence time ( Time format: YYYY-MM-DDTHH:MM:SSZ)

  • sw-show-alert

    Get info about a specific alert by its ID

  • sw-unblock-domain

    Remove a domain from the block list

  • sw-update-alert

    Update an alert

import demistomock as demisto
from CommonServerPython import *

""" IMPORTS """
import collections
import json
import os
from datetime import datetime, timedelta

import requests
import urllib3

urllib3.disable_warnings()

""" GLOBAL VARS """
SERVER = demisto.params().get("serverURL", "").strip("/")
SERVER_URL = SERVER + "/api/v3"
API_KEY = demisto.params().get("credentials_api_key", {}).get("password") or demisto.params().get("APIKey")

USE_SSL = not demisto.params().get("insecure")

DEFAULT_HEADERS = {"Content-Type": "application/json", "Accept": "application/json", "Authorization": API_KEY}

""" HELPER FUNCTIONS """

if not demisto.params().get("proxy"):
    # Remove proxy environment variables if they exist
    for proxy_var in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]:
        os.environ.pop(proxy_var, None)


def http_request(method, url_suffix, params_dict=None, headers=DEFAULT_HEADERS, data=None):
    req_params = {}  # type: Dict[Any,Any]
    if params_dict is not None:
        req_params.update(params_dict)

    url = SERVER_URL + url_suffix

    LOG(f"running {method} request with url={url}\tparams={json.dumps(req_params)}")

    try:
        res = requests.request(method, url, verify=USE_SSL, params=req_params, headers=headers, data=data)
        res.raise_for_status()
        try:
            return res.json()
        except ValueError:
            # in case the response doesn't have JSON
            return "Request completed"
    except Exception as e:
        LOG(e)
        raise (e)


def underscore_to_camelcase(word):
    return " ".join(x.capitalize() or "_" for x in word.split("_"))


def create_incident_data_from_alert(alert):
    alert.pop("comments")
    alert.pop("observations")
    return {
        "name": "Stealthwatch alert " + str(alert.get("id", "")),
        "rawJSON": json.dumps(alert),
        "occurred": alert.get("created", ""),
    }


def get_latest_id(alerts_data):
    latest_id = 0
    for alert in alerts_data:
        current_id = alert.get("id", None)
        if current_id is not None and current_id > latest_id:
            latest_id = current_id

    return latest_id


""" COMMANDS FUNCTIONS """


def show_alert(alert_id):
    """
    Returns alert by specific id
    """

    api_endpoint = f"/alerts/alert/{alert_id}/"
    return http_request("GET", api_endpoint, {}, DEFAULT_HEADERS)


def show_alert_command():
    """
    corresponds to 'sw-show-alert' command. Returns information about a specific alert
    """
    alert_id = demisto.args().get("alertID")

    alert_data = show_alert(alert_id)

    if demisto.args().get("addComments", False) != "true":
        alert_data.pop("comments")
        alert_data.pop("new_comment")

    alert_data.pop("observations")

    list_for_md = ["resolved", "id", "last_modified", "obj_created", "assigned_to"]

    dict_for_md = {underscore_to_camelcase(k): v for k, v in alert_data.items() if k in list_for_md}
    md = tableToMarkdown(alert_data.get("text", ""), dict_for_md)

    return {
        "Type": entryTypes["note"],
        "Contents": alert_data,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": md,
        "EntryContext": {"Stealthwatch.Alert(val.id==obj.id)": alert_data},
    }


def update_alert(alert_id, data):
    """
    Updates alert by specific id
    """

    api_endpoint = f"/alerts/alert/{alert_id}/"
    return http_request("PUT", api_endpoint, data=json.dumps(data))


def update_alert_command():
    """
    corresponds to 'sw-update-alert' command. Returns information about a specific alert
    """
    args = demisto.args()
    alert_id = args.get("alertID")
    update_params = {}
    # adding the possible params for update
    possible_params = ["new_comment", "tags", "publish_time", "resolved", "snooze_settings", "merit", "assigned_to"]
    for param in possible_params:
        current_param = args.get(param, False)
        if current_param:
            update_params[param] = current_param
    username = args.get("resolved_user", None)
    if username is not None:
        update_params["resolved_user"] = {"username": username}

    alert_data = update_alert(alert_id, update_params)

    alert_data.pop("comments")
    alert_data.pop("new_comment")
    alert_data.pop("observations")

    list_for_md = ["resolved", "id", "last_modified", "obj_created", "assigned_to"]

    dict_for_md = {k: v for k, v in alert_data.items() if k in list_for_md}
    md = tableToMarkdown(alert_data.get("text", ""), dict_for_md)

    return {
        "Type": entryTypes["note"],
        "Contents": alert_data,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": md,
        "EntryContext": {"Stealthwatch.Alert(val.id==obj.id)": alert_data},
    }


def list_alerts(params):
    """
    Retrieves alerts
    """

    api_endpoint = "/alerts/alert/"
    return http_request("GET", api_endpoint, params, DEFAULT_HEADERS)


def build_alert_dic(alert):
    dic = collections.OrderedDict()  # type: Dict[str,str]
    list_for_md = ["id", "last_modified", "resolved", "text", "obj_created", "assigned_to", "description"]
    for item in list_for_md:
        dic[underscore_to_camelcase(item)] = alert[item]

    return dic


def list_alerts_command():
    """
    corresponds to 'sw-list-alerts' command. Returns a list of Stealthwatch alerts
    """
    args = demisto.args()
    list_params = {}
    # adding the possible params for update
    possible_params = ["status", "tags", "search", "assignee", "limit"]
    for param in possible_params:
        current_param = args.get(param, False)
        if current_param:
            list_params[param] = current_param

    alerts_data = list_alerts(list_params).get("objects")
    md_dicts_list = []

    for alert in alerts_data:
        if demisto.args().get("addComments", False) != "true":
            alert.pop("comments")
            alert.pop("new_comment")
        alert.pop("observations")
        md_dicts_list.append(build_alert_dic(alert))

    md = tableToMarkdown("The following alerts were retrieved", md_dicts_list)
    return {
        "Type": entryTypes["note"],
        "Contents": alerts_data,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": md,
        "EntryContext": {"Stealthwatch.Alert(val.id==obj.id)": alerts_data},
    }


def domain_block(params):
    """
    Updates domain blacklist status
    """

    api_endpoint = "/blacklist/domains/"
    return http_request("POST", api_endpoint, {}, DEFAULT_HEADERS, params)


def block_domain_command():
    """
    corresponds to 'sw-block-domain-or-ip' command. Adds a domain to the blacklist
    """
    domain = demisto.args().get("domain")
    ip = demisto.args().get("ip")

    if not (domain or ip):
        return {"Type": entryTypes["error"], "ContentsFormat": formats["text"], "Contents": "Please enter either domain or ip"}

    if domain and ip:
        return {
            "Type": entryTypes["error"],
            "ContentsFormat": formats["text"],
            "Contents": "Please enter only domain or ip, not both",
        }

    identifier = None
    if domain:
        identifier = domain
    else:
        identifier = ip

    domain_params = {"identifier": identifier, "category": "domain", "list_on": "blacklist"}

    domain_result = domain_block(json.dumps(domain_params))

    ec = None

    if domain:
        ec = {"Stealthwatch.Domain(val.identifier==obj.identifier)": domain_result}
    else:
        ec = {"Stealthwatch.IP(val.identifier==obj.identifier)": domain_result}

    return {
        "Type": entryTypes["note"],
        "Contents": domain_result,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Blacklist " + domain + " result", domain_result),
        "EntryContext": ec,
    }


def domain_unblock(domain_id):
    """
    Removes domain from the blacklist
    """

    api_endpoint = f"/blacklist/domains/{domain_id}/"
    return http_request("DELETE", api_endpoint, None, DEFAULT_HEADERS, None)


def unblock_domain_command():
    """
    corresponds to 'sw-unblock-domain' command. Removes a domain to the blacklist
    """
    domain_id = demisto.args().get("id")

    domain_result = domain_unblock(domain_id)

    return {
        "Type": entryTypes["note"],
        "Contents": domain_result,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "Unblocked domain with id: " + domain_id,
    }


def list_domains(list_params):
    """
    Lists blacklisted domains
    """

    api_endpoint = "/blacklist/domains/"
    return http_request("GET", api_endpoint, list_params, DEFAULT_HEADERS, {})


def list_blocked_domains_command():
    """
    corresponds to 'sw-list-blocked-domains' command. Returns a list of the blocked domains
    """
    args = demisto.args()
    list_params = {}
    # adding the possible params for update
    possible_params = ["search", "limit"]
    for param in possible_params:
        current_param = args.get(param, False)
        if current_param:
            list_params[param] = current_param

    specific_domain = args.get("domain", None)
    if specific_domain is not None:
        list_params["identifier"] = specific_domain

    domains_data = list_domains(list_params)

    domains_result = domains_data.get("objects", {})

    data_output = []
    for obs in domains_result:
        data_output.append({underscore_to_camelcase(k): v for k, v in list(obs.items())})

    return {
        "Type": entryTypes["note"],
        "Contents": domains_data,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Current blacklisted domains are", data_output),
        "EntryContext": {"Stealthwatch.Domain(val.identifier==obj.identifier)": domains_result},
    }


def list_observations(params):
    """
    Lists observations
    """

    api_endpoint = "/observations/all/"
    return http_request("GET", api_endpoint, params, DEFAULT_HEADERS)


def list_observations_command():
    """
    corresponds to 'sw-list-observations' command. Returns a list of Stealthwatch observations
    """
    args = demisto.args()
    list_params = {"ordering": "creation_time"}
    # adding the possible params for update
    possible_params = ["alert", "id", "search", "limit"]
    for param in possible_params:
        current_param = args.get(param, False)
        if current_param:
            list_params[param] = current_param

    observations_data = list_observations(list_params).get("objects")

    data_output = []
    for obs in observations_data:
        data_output.append({underscore_to_camelcase(k): v for k, v in list(obs.items())})

    return {
        "Type": entryTypes["note"],
        "Contents": data_output,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Found the following observations", data_output),
        "EntryContext": {"Stealthwatch.Observation(val.id==obj.id)": observations_data},
    }


def list_sessions(params):
    """
    Lists observations
    """

    api_endpoint = "/snapshots/session-data/"
    return http_request("GET", api_endpoint, params, DEFAULT_HEADERS)


def list_sessions_command():
    """
    corresponds to 'sw-list-sessions' command. Returns a list of Stealthwatch
    sessions
    """
    date_format = "%Y-%m-%dT%H:%M:%SZ"
    list_params = {}

    ip = demisto.args().get("ip")
    connected_ip = demisto.args().get("connectedIP")
    connected_device_id = demisto.args().get("connectedDeviceId")
    limit = demisto.args().get("limit")
    start_time = demisto.args().get("startTime", None)
    end_time = demisto.args().get("endTime", None)
    session_type = demisto.args().get("sessionType", "all")

    if start_time and end_time:
        list_params["start_datetime"] = start_time
        list_params["end_datetime"] = end_time
    elif end_time is None:
        start_time_object = datetime.strptime(start_time, date_format)
        start_time_object = start_time_object - timedelta(minutes=5)
        end_time_object = start_time_object + timedelta(minutes=5)
        start_time = start_time_object.strftime(date_format)
        end_time = end_time_object.strftime(date_format)

    list_params["ip"] = ip
    list_params["connected_ip"] = connected_ip
    list_params["limit"] = limit
    list_params["start_datetime"] = start_time
    list_params["end_datetime"] = end_time
    list_params["connected_device_id"] = connected_device_id

    unique_session_ids = []  # type: List[str]
    final_sessions_data = []
    sessions_data = list_sessions(list_params).get("objects")
    for sess in sessions_data:
        if sess["connected_ip"] not in unique_session_ids:
            unique_session_ids.append(sess["connected_ip"])
            if demisto.get(sess, "connected_device_id"):
                sess["connected_device_is_external"] = False
                if session_type == "internal":
                    final_sessions_data.append(sess)
            else:
                sess["connected_device_is_external"] = True
                if session_type == "external":
                    final_sessions_data.append(sess)
            if session_type == "all":
                final_sessions_data.append(sess)

    data_output = []
    for sess in final_sessions_data:
        data_output.append({underscore_to_camelcase(k): v for k, v in list(sess.items())})

    return {
        "Type": entryTypes["note"],
        "Contents": data_output,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Found the following session data", data_output),
        "EntryContext": {"Stealthwatch.Session(val.id==obj.id)": final_sessions_data},
    }


def fetch_incidents():
    date_format = "%Y-%m-%dT%H:%M:%SZ"

    list_params = {"ordering": "created", "limit": 100}
    final_alerts = []
    last_fetch_string = demisto.getLastRun().get("last_fetch_time", None)
    ids = demisto.getLastRun().get("ids", None)
    first_time = not last_fetch_string and ids is not None

    if last_fetch_string is None or not last_fetch_string:
        now = datetime.now()
        last_fetch = now - timedelta(days=20)
    else:
        last_fetch = parse_date_string(last_fetch_string)

    # Couldn't find a way to sort descending so looking for last offset of 100 alerts
    alerts_response = list_alerts(list_params)
    num_alerts = alerts_response.get("meta", {"total_count": 100}).get("total_count")
    offset = 0 if num_alerts < 100 else num_alerts - 100
    list_params["offset"] = offset
    alerts_response = list_alerts(list_params)

    alerts_data = alerts_response.get("objects", [])
    max_fetch_time = last_fetch_string if last_fetch_string else now.strftime(date_format)

    for alert in alerts_data:
        created = alert.get("created")
        if parse_date_string(created) > last_fetch:
            incident_from_alert = create_incident_data_from_alert(alert)
            if first_time:
                if alert.get("id") not in ids:
                    final_alerts.append(incident_from_alert)
            else:
                final_alerts.append(incident_from_alert)
            if parse_date_string(created) > parse_date_string(max_fetch_time):
                max_fetch_time = created

    demisto.setLastRun({"last_fetch_time": max_fetch_time})
    demisto.incidents(final_alerts)


""" EXECUTION CODE """


def main():
    demisto.debug(f"Command being called is {demisto.command()}")
    if not API_KEY:
        raise DemistoException("Stealthwatch Cloud API key must be provided.")
    try:
        if demisto.command() == "test-module":
            # This is the call made when pressing the integration test button.
            if list_alerts_command():
                demisto.results("ok")
            else:
                demisto.results("test failed")
        elif demisto.command() == "sw-show-alert":
            demisto.results(show_alert_command())
        elif demisto.command() == "sw-update-alert":
            demisto.results(update_alert_command())
        elif demisto.command() == "sw-list-alerts":
            demisto.results(list_alerts_command())
        elif demisto.command() == "sw-block-domain-or-ip":
            demisto.results(block_domain_command())
        elif demisto.command() == "sw-unblock-domain":
            demisto.results(unblock_domain_command())
        elif demisto.command() == "sw-list-blocked-domains":
            demisto.results(list_blocked_domains_command())
        elif demisto.command() == "sw-list-observations":
            demisto.results(list_observations_command())
        elif demisto.command() == "sw-list-sessions":
            demisto.results(list_sessions_command())
        elif demisto.command() == "fetch-incidents":
            demisto.results(fetch_incidents())
    except Exception as e:
        return_error(f"error has occurred: {e!s}")


if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    main()