Thinkst Canary

By presenting itself as an apparently benign and legitimate service(s), the Canary draws the attention of unwanted activity. When someone trips one of the Canary's triggers, an alert is sent to notify the responsible parties so that action can be taken before valubale systems in your network are compromised.

Deception & Breach Simulation · Thinkst Canary

Details

IDThinkst Canary
ProviderThinkst Canary
CategoryDeception & Breach Simulation
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

By presenting itself as an apparently benign and legitimate service(s), the canary draws the attention of unwanted activity. When someone trips one of the Canary’s triggers, an alert is sent to notify the responsible parties so that action can be taken before valuable systems in your network are compromised.

Use Cases

  • Fetch alerts from CanaryTools as incidents in Cortex XSOAR and acknowledge them.
  • Get information about all registered Canaries.
  • Get information about Canary Tokens.
  • Add IP addresses to allow list.

Configure Thinkst Canary on Cortex XSOAR

For information about how to get your Authentication Token, see the Canary documentation.

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Thinkst Canary.
  3. Click Add instance to create and configure a new integration instance.
    • Name: A textual name for the integration instance.
    • Fetch incidents: Should the instance fetch incidents or not
    • Incident type: Choose a type for Canary Tools incidents
    • Canary Server URL
    • API auth token
    • Trust any certificate (not secure)
    • Use system proxy settings
  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. List all registered Canaries


Lists all registered Canaries.

Base Command

canarytools-list-canaries

Input

There are no input arguments for this command.

Context Output
Path Type Description
CanaryTools.Device.ID string Device ID
CanaryTools.Device.Name string Device name
CanaryTools.Device.Description string Device description
CanaryTools.Device.Address string Device IP address
CanaryTools.Device.Status boolean Device status - Live (True/False)
CanaryTools.Device.Location string Device location
CanaryTools.Device.Version string Device version
CanaryTools.Device.LastSeen date Device last seen time
CanaryTools.Device.LastUpdated date Device last updated time

 

Command Example
!canarytools-list-canaries
Context Example
{  
   "CanaryTools":{  
      "Device":{  
         "Status":true,
         "Description":"Lab",
         "LastUpdated":"2019-02-24 16:46:01 UTC+0000",
         "Version":"2.1.2",
         "Location":"Lab",
         "Address":"192.168.1.43",
         "Name":"VirtualCanary-1",
         "ID":"0002f07cb32d45b1",
         "LastSeen":"Sun Feb 24 2019 16:46:01 GMT+0000 (UTC)"
      }
   }
}
Human Readable Output

image

2. List all Canary tokens


Lists all Canary tokens.

Base Command

canarytools-list-tokens

Input

There are no input arguments for this command.

Context Output
Path Type Description
CanaryTools.Tokens.CanaryToken string Canary Token
CanaryTools.Tokens.CreatedTime date Token Created time
CanaryTools.Tokens.Status boolean Token status - Enabled (True / False)
CanaryTools.Tokens.Kind string Token Kind
CanaryTools.Tokens.Triggered number Token triggered count
CanaryTools.Tokens.DocName string Token document name (If the token is from type document)
CanaryTools.Tokens.TokenURL string Token URL (How the token is presented)

 

Command Example
!canarytools-list-tokens
Context Example
{  
   "CanaryTools":{  
      "Tokens":[  
         {  
            "Status":true,
            "Kind":"doc-msword",
            "Triggered":16,
            "TokenURL":"http://e71ed0532685.o3n.io/cdn/ows8bhzvpx9ro3nz32r5vb3pa/logo.gif",
            "DocName":"token.docx",
            "CanaryToken":"ows8bhzvpx9ro3nz32t5vb3pa",
            "CreatedTime":"2019-01-23 15:51:31 (UTC)"
         }
      ]
   }
}
Human Readable Output

image

3. Check if an IP address is on allow list


Checks whether a given IP address and port are on allow list.

Base Command

canarytools-check-whitelist

Input
Argument Name Description Required
ip IP address Required
port Destination port Optional

 

Context Output
Path Type Description
CanaryTools.IP.Address string IP address
CanaryTools.IP.Port string Destination port for the IP allow list
CanaryTools.IP.Whitelisted boolean Is the IP address on allow list (true/false)

 

Command Example
!canarytools-check-whitelist ip=1.1.1.1 port=45
Context Example
{  
   "CanaryTools":{  
      "IP":{  
         "Whitelisted":"True",
         "Port":"45",
         "Address":"1.1.1.1"
      }
   }
}
Human Readable Output

image

4. Add an IP address to the allow list


Adds an IP address to the allow list in Canary.

Base Command

canarytools-whitelist-ip

Input
Argument Name Description Required
ip IP address to allow list Required
port Destination port to allow list Optional

 

Context Output
Path Type Description
CanaryTools.IP.Address string IP address
CanaryTools.IP.Port string Destination port for the IP allow list
CanaryTools.IP.Whitelisted boolean Is the IP address on allow list (True/False)

 

Command Example
!canarytools-whitelist-ip ip=2.2.2.2 port=21
Context Example
{  
   "CanaryTools":{  
      "IP":{  
         "Whitelisted":"True",
         "Port":"21",
         "Address":"2.2.2.2"
      }
   }
}
Human Readable Output

image

5. Edit an alert status


Edits the status for an alert in Canary Tools.

Base Command

canarytools-edit-alert-status

Input
Argument Name Description Required
alertID Alert ID (e.g., incident:canarytoken:d6fe0ae4dfd36cc3cc6d9d4f::1548593719) Required
status Required status for the alert (Acknowledge, Unacknowledge) Required

 

Context Output
Path Type Description
CanaryTools.Alert.ID string Alert ID
CanaryTools.Alert.Status string Alert status

 

Command Example
!canarytools-edit-alert-status alertID=incident:canarytoken:d6fe0ae4dfd36cc3cc6d9d4f::1548593719 status=Acknowledge
Context Example
{  
   "CanaryTools":{  
      "Alert":{  
         "Status":"Acknowledge",
         "ID":"incident:canarytoken:d6fe0ae4dfd36cc3cc6d9d4f::1548593719"
      }
   }
}
Human Readable Output

image

6. Get a Canary Token file


Fetches a Canary Token file from the Canary Tools server.

Base Command

canarytools-get-token

Input
Argument Name Description Required
token Canary Token Required

 

Context Output
Path Type Description
CanaryTools.Tokens.CanaryToken string Canary Token
File.Size string File Size
File.SHA1 string File SHA-1
File.SHA256 string File SHA-256
File.Name string File name
File.SSDeep string File SSDeep
File.EntryID string File EntryID
File.Info string File info
File.Type string File type
File.MD5 string File MD5
File.Extension string File extension

 

Command Example
!canarytools-get-token token=wpdkr30rx9naixdsijxdbd5ab
Context Example
{  
   "CanaryTools":{  
      "Tokens":"wpdkr30rx9naixdsijxwbd5ab"
   },
   "File":{  
      "Info":"image/jpeg",
      "SHA1":"9719f38b13a9ab79469987a1ba495939c8577c54",
      "Name":"affinity-photo-161120170952.jpg",
      "Extension":"jpg",
      "Size":240804,
      "EntryID":"163@f7519b11-9105-4dd4-8036-fe2790f28ca6",
      "SSDeep":"6144:IIgHoF7QKgKBiWlCdA1xQe5S55nXxlQIqwUAlP:InIFHriAGALT2Xd5P",
      "SHA256":"cacf61ee474920578bd0e6f000b65fa40b313b912a8611b930414a7b9ae1d49b",
      "Type":"ASCII text, with very long lines, with no line terminators\n",
      "MD5":"c499c7f42eae921c974a4e71cb4a4cc5"
   }
}
Human Readable Output

image

Configuration parameters

  • server — Canary Server URL (e.g., https://***.canary.tools) (required)
  • auth_token — API Authentication Token
  • authentication_token
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • fetchDelta — First fetch time (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year)
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (6)

  • canarytools-check-whitelist

    Checks whether a given IP address and port are on allow list.

  • canarytools-edit-alert-status

    Edits the status for an alert in Canary Tools.

  • canarytools-get-token

    Fetches a Canary Token file from the Canary Tools server.

  • canarytools-list-canaries

    Lists all registered Canaries.

  • canarytools-list-tokens

    Lists all Canary tokens.

  • canarytools-whitelist-ip

    Adds an IP address to the allow list in Canary.

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

from CommonServerUserPython import *

""" IMPORTS """
import requests

# disable insecure warnings
urllib3.disable_warnings()

# remove proxy if not set to true in params
if not demisto.params().get("proxy", False):
    # 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)

""" GLOBALS"""

SERVER = demisto.params().get("server").rstrip("/") + "/api/v1/"
VERIFY_CERTIFICATE = not demisto.params().get("insecure", True)
FETCH_DELTA = demisto.params().get("fetchDelta", "24 hours").strip()
RELEVANT_DEVICE_ENTRIES = {
    "description": "Description",
    "id": "ID",
    "ip_address": "Address",
    "last_seen": "LastSeen",
    "live": "Status",
    "location": "Location",
    "name": "Name",
    "updated_std": "LastUpdated",
    "version": "Version",
}
RELEVANT_TOKEN_ENTRIES = {
    "canarytoken": "CanaryToken",
    "created_printable": "CreatedTime",
    "enabled": "Enabled",
    "kind": "Kind",
    "triggered_count": "Triggered",
    "doc_name": "DocName",
    "url": "TokenURL",
}
DEF_PARAMS = {
    "auth_token": demisto.params().get("authentication_token", {}).get("password") or demisto.params().get("auth_token", None)
}
if not DEF_PARAMS["auth_token"]:
    raise DemistoException("API Authentication Token must be provided.")
"""HELPER FUNCTIONS"""


def http_request(method, url, params=None):  # pragma: no cover
    """
    HTTP request helper function
    """
    if params is None:
        params = DEF_PARAMS
    else:
        params.update(DEF_PARAMS)
    res = requests.request(method=method, url=url, params=params, verify=VERIFY_CERTIFICATE)

    if not res.ok:
        try:
            res_json = res.json()
            if "message" in res_json:
                LOG(str(res.text))
                LOG(res_json.get("message"))
                return_error(res_json.get("message"))
        except ValueError:
            LOG(str(res.text))
            return_error(str(res.text))
        except Exception as ex:
            LOG(res.text)
            return_error(str(ex))

    try:
        res_json = res.json()
        return res_json
    except Exception as ex:
        demisto.debug(str(ex))
        return_error(str(ex))


def get_alerts(last_fetch=None):
    """
    Retrieve all unacknowledged alerts from Canary Tools
    :param last_fetch: Last fetch incidents time
    """

    if last_fetch:
        params = {"newer_than": last_fetch}
        res = http_request("GET", SERVER + "incidents/unacknowledged", params)
    else:
        res = http_request("GET", SERVER + "incidents/unacknowledged")
    alerts = res.get("incidents")
    return alerts


def create_incident(alert):
    """
    Turns an alert from Canary Tools to the incident structure in Demisto
    :return: Demisto incident, e.g., CanaryToken triggered
    """
    incident = {
        "name": demisto.get(alert, "description.description"),
        "occurred": timestamp_to_datestring(1000 * (int(demisto.get(alert, "description.created")))),
        "rawJSON": json.dumps(alert),
    }
    return incident


"""COMMANDS"""


def test_module():  # pragma: no cover
    try:
        res = requests.request("GET", SERVER + "ping", params=DEF_PARAMS, verify=VERIFY_CERTIFICATE)
        if not res.ok:
            try:
                res_json = res.json()
                return_error("Could not connect, reason: {}".format(res_json.get("message")))

            except Exception as ex:
                demisto.debug(str(ex))
                return_error("Could not parse server response, please verify instance parameters")
        demisto.results("ok")
    except Exception as ex:
        demisto.debug(str(ex))
        return_error("Failed to establish new connection, please verify instance parameters")


def list_canaries():
    """
    Retrieve all Canaries available in Canary Tools
    :return: json response, a list of all devices
    """
    res = http_request("GET", SERVER + "devices/all")
    new_devices = [
        {new_key: device.get(old_key, None) for old_key, new_key in list(RELEVANT_DEVICE_ENTRIES.items())}
        for device in res["devices"]
    ]
    return res, new_devices


def list_canaries_command():
    """
    Retrieve all Canaries available in Canary Tools
    """
    res_json, new_devices = list_canaries()
    context = createContext(new_devices, removeNull=True)
    headers = ["ID", "Name", "Description", "Address", "Status", "Location", "Version", "LastSeen", "LastUpdated"]
    contents = res_json
    human_readable = tableToMarkdown("Canary Devices", new_devices, headers=headers)
    outputs = {"CanaryTools.Device(val.ID && val.ID === obj.ID)": context}
    return_outputs(readable_output=human_readable, outputs=outputs, raw_response=contents)


def list_tokens():
    """
    Retrieve all Canary Tokens available in Canary Tools
    :return: json response, a list of all tokens
    """
    res = http_request("GET", SERVER + "canarytokens/fetch")
    new_tokens = []
    for token in res["tokens"]:
        new_tokens.append({new_key: token.get(old_key, None) for old_key, new_key in list(RELEVANT_TOKEN_ENTRIES.items())})
    return res, new_tokens


def list_tokens_command():
    """
    Retrieve all Canary Tokens available in Canary Tools
    """
    res_json, new_tokens = list_tokens()
    headers = sorted(new_tokens[0].keys()) if new_tokens else None
    context = createContext(new_tokens, removeNull=True)

    contents = res_json
    for token in new_tokens:
        token.pop("TokenURL", None)
    human_readable = tableToMarkdown("Canary Tools Tokens", new_tokens, headers=headers, removeNull=True)
    outputs = {"CanaryTools.Token(val.CanaryToken && val.CanaryToken === obj.CanaryToken)": context}
    return_outputs(readable_output=human_readable, outputs=outputs, raw_response=contents, ignore_auto_extract=True)


def get_token_command():
    """
    Fetch a Canary Token from the Canary Tools server
    :return: Canary Token information or file
    """
    token = demisto.args().get("token")
    params = {"canarytoken": token}
    res = http_request("GET", SERVER + "canarytoken/fetch", params=params)
    context = demisto.get(res, "token.canarytoken")
    contents = res
    human_readable = "File Fetched Successfully"
    outputs = {"CanaryTools.Token(val.CanaryToken && val.CanaryToken === obj.CanaryToken)": context}

    if demisto.get(res, "token.doc"):
        name = demisto.get(res, "token.doc_name")
        content = demisto.get(res, "token.doc")
        token_file = fileResult(name, content)
        demisto.results(token_file)
    if demisto.get(res, "token.web_image"):
        name = demisto.get(res, "token.web_image_name")
        content = demisto.get(res, "token.web_image")
        token_file = fileResult(name, content)
        demisto.results(token_file)
    else:
        token_data = res.get("token", [])
        token_data.pop("url", None)
        human_readable = tableToMarkdown("Canary Tools Tokens", token_data)

    return_outputs(readable_output=human_readable, outputs=outputs, raw_response=contents, ignore_auto_extract=True)


def check_whitelist(ip, port):
    """
    Check if a given IP address is whitelisted in Canary Tools
    :return: json response
    """
    params = {"src_ip": ip, "dst_port": port}

    res = http_request("GET", SERVER + "settings/is_ip_whitelisted", params=params)
    return res


def check_whitelist_command():
    """
    Check if a given IP address is whitelisted in Canary Tools
    """
    ip = demisto.args().get("ip")
    port = demisto.args().get("port")
    res = check_whitelist(ip, port)

    if not port:
        port = "Any"
    # is_ip_ignored key determines whether the ip is whitelisted or not.
    context = {"Address": str(ip), "Port": str(port), "Whitelisted": str(res.get("is_ip_ignored"))}
    contents = res
    context = createContext(context, removeNull=True)
    outputs = {"CanaryTools.IP(val.Address && val.Address===obj.Address && val.Port && val.Port===obj.Port)": context}

    if res.get("is_ip_ignored"):
        human_readable = f"The IP address {ip}:{port} is Whitelisted"
    else:
        human_readable = f"The IP address {ip}:{port} is not Whitelisted"

    return_outputs(readable_output=human_readable, outputs=outputs, raw_response=contents)


def whitelist_ip(ip, port):
    """
    Whitelist an IP address in Canary Tools
    :return: json response
    """
    params = {"src_ip": ip, "dst_port": port}

    res = http_request("POST", SERVER + "settings/whitelist_ip_port", params=params)
    return res


def whitelist_ip_command():
    """
    Whitelist an IP address in Canary Tools
    """
    ip = demisto.args().get("ip")
    port = demisto.args().get("port")
    res = whitelist_ip(ip, port)

    if not port:
        port = "Any"

    result_status = res.get("result")
    if result_status == "success":
        context = {"Address": str(ip), "Port": str(port), "Whitelisted": "True"}
        context = createContext(context, removeNull=True)
        contents = res
        human_readable = f"The IP address {ip}:{port} was added to the Whitelist"
        outputs = {"CanaryTools.IP(val.Address && val.Address===obj.Address && val.Port && val.Port===obj.Port)": context}
        return_outputs(readable_output=human_readable, outputs=outputs, raw_response=contents)

    elif result_status == "failure":
        return_outputs(readable_output=res.get("message"), outputs={}, raw_response=res)

    elif result_status == "error":
        return_error(res.get("message"))


def alert_status_command():
    """
    Acknowledge or Uncknowledge an Alert in Canary Tools
    """
    args = demisto.args()
    alert = args.get("alert_id")
    status = args.get("status")
    context = {
        "ID": str(alert),
        "Status": str(status),
    }
    context = createContext(context, removeNull=True)
    params = {"incident": alert}
    if status == "Acknowledge":
        res = http_request("POST", SERVER + "incident/acknowledge", params=params)
        if res.get("action") == "acknowledged":
            contents = res
            human_readable = f"The Alert {alert} was " + res.get("action")
            outputs = {"CanaryTools.Alert(val.ID && val.ID === obj.ID)": context}
            return_outputs(readable_output=human_readable, outputs=outputs, raw_response=contents)

    elif status == "Unacknowledge":
        res = http_request("POST", SERVER + "incident/unacknowledge", params=params)
        if res.get("action") == "unacknowledged":
            contents = res
            human_readable = f"The Alert {alert} was " + res.get("action")
            outputs = {"CanaryTools.Alert(val.ID && val.ID === obj.ID)": context}
            return_outputs(readable_output=human_readable, outputs=outputs, raw_response=contents)
        else:
            return_error("Unsupported command")


def fetch_incidents_command():
    """
    Fetch alerts from Canary Tools as incidents in Demisto
    last_fetch: The latest fetched alert creation time
    """
    last_fetch = demisto.getLastRun().get("time")

    if last_fetch is None:
        last_fetch = parse_date_range(FETCH_DELTA, "%Y-%m-%d-%H:%M:%S")[0]

    # All alerts retrieved from get_alerts are newer than last_fetch and are in a chronological order
    alerts = get_alerts(last_fetch)

    incidents = []
    current_fetch = last_fetch
    for alert in alerts:
        current_fetch = 1000 * (int(demisto.get(alert, "description.created")) + 1)
        current_fetch = timestamp_to_datestring(current_fetch, "%Y-%m-%d-%H:%M:%S")
        incident = create_incident(alert)
        incidents.append(incident)

    demisto.incidents(incidents)
    demisto.setLastRun({"time": current_fetch})


# Execution Code
try:
    if demisto.command() == "test-module":
        test_module()
    elif demisto.command() == "canarytools-list-tokens":
        list_tokens_command()
    elif demisto.command() == "canarytools-get-token":
        get_token_command()
    elif demisto.command() == "canarytools-list-canaries":
        list_canaries_command()
    elif demisto.command() == "canarytools-check-whitelist":
        check_whitelist_command()
    elif demisto.command() == "canarytools-whitelist-ip":
        whitelist_ip_command()
    elif demisto.command() == "canarytools-edit-alert-status":
        alert_status_command()
    elif demisto.command() == "fetch-incidents":
        fetch_incidents_command()
except Exception as e:
    return_error(f"Unable to perform command : {demisto.command}, Reason: {e}")