Synapse

Synapse intelligence analysis platform.

Data Enrichment & Threat Intelligence · Synapse

Details

IDSynapse
ProviderSynapse Financial Technologies
CategoryData Enrichment & Threat Intelligence
From Version5.0.0
Docker Imagedemisto/py3-tools:1.0.0.117220
Supported ModulesAgentix XSIAM

README

Synapse intelligence analysis platform.
This integration was integrated and tested with version 2.7.0 of Synapse

Configure Synapse in Cortex

Parameter Description Required
url Server URL (e.g. https://synapse.vertex.link) True
port REST API Port (default is 4443). True
credentials Username and password to user to authenticate to Synapse. True
insecure Trust any certificate (not secure) False
proxy Use system proxy settings False
timezone Timezone (optional) False
bad_tag Malicious Tag False
good_tag Benign Tag False
use_optic Synapse is running Optic 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.

ip


Returns IP information and reputation.

Base Command

ip

Input

Argument Name Description Required
ip List of IPs. Required

Context Output

Path Type Description
Synapse.IP.ip String The IP address of the indicator.
Synapse.IP.tags String The tags applied to the IP address.
DBotScore.Indicator String The value assigned by DBot for the indicator.
DBotScore.Type String The type assigned by DBot for the indicator.
DBotScore.Score Number The score assigned by DBot for the indicator.
DBotScore.Vendor String The vendor used to calculate the score.
IP.Address string The IP address of the indicator.
IP.Malicious.Vendor string For malicious IP addresses, the vendor that made the decision.
IP.Malicious.Description string For malicious IP addresses, the full description.

Command Example

!ip ip="1.2.3.4"

Context Example

{
    "DBotScore": {
        "Indicator": "1.2.3.4",
        "Score": 3,
        "Type": "ip",
        "Vendor": "Synapse"
    },
    "IP": {
        "Address": "1.2.3.4",
        "Malicious": {
            "Description": "Synapse returned reputation tag: mal",
            "Vendor": "Synapse"
        }
    },
    "Synapse": {
        "IP": {
            "ip": "1.2.3.4",
            "tags": [
                "mal",
                "test"
            ]
        }
    }
}

Human Readable Output

IP List

ip tags
1.2.3.4 mal,
test

url


Returns URL information and reputation.

Notice: Submitting indicators using this command might make the indicator data publicly available. See the vendor’s documentation for more details.

Base Command

url

Input

Argument Name Description Required
url List of URLs. Required

Context Output

Path Type Description
Synapse.URL.url String The data of the URL indicator.
Synapse.URL.tags String The tags applied to the url.
DBotScore.Indicator String The value assigned by DBot for the indicator.
DBotScore.Type String The type assigned by DBot for the indicator.
DBotScore.Score Number The score assigned by DBot for the indicator.
DBotScore.Vendor String The vendor used to calculate the score.
URL.Data string The data of the URL indicator.
URL.Malicious.Vendor string For malicious URLs, the vendor that made the decision.
URL.Malicious.Description string For malicious URLs, the full description.

Command Example

!url url="https://google.com"

Context Example

{
    "DBotScore": {
        "Indicator": "https://google.com",
        "Score": 0,
        "Type": "url",
        "Vendor": "Synapse"
    },
    "Synapse": {
        "URL": {
            "tags": [],
            "url": "https://google.com"
        }
    },
    "URL": {
        "Data": "https://google.com"
    }
}

Human Readable Output

URL List

tags url
  https://google.com

domain


Returns Domain information and reputation.

Notice: Submitting indicators using this command might make the indicator data publicly available. See the vendor’s documentation for more details.

Base Command

domain

Input

Argument Name Description Required
domain List of Domains. Required

Context Output

Path Type Description
Synapse.Domain.domain String The fully qualified domain name.
Synapse.Domain.tags String The tags applied to the domain.
DBotScore.Indicator String The value assigned by DBot for the indicator.
DBotScore.Type String The type assigned by DBot for the indicator.
DBotScore.Score Number The score assigned by DBot for the indicator.
DBotScore.Vendor String The vendor used to calculate the score.
Domain.Name string The name of the domain.
Domain.Malicious.Vendor string For malicious domains, the vendor that made the decision.
Domain.Malicious.Description string For malicious domains, the full description.

Command Example

!domain domain="foobar.com"

Context Example

{
    "DBotScore": {
        "Indicator": "foobar.com",
        "Score": 3,
        "Type": "domain",
        "Vendor": "Synapse"
    },
    "Domain": {
        "Malicious": {
            "Description": "Synapse returned reputation tag: mal",
            "Vendor": "Synapse"
        },
        "Name": "foobar.com"
    },
    "Synapse": {
        "Domain": {
            "domain": "foobar.com",
            "tags": [
                "mal"
            ]
        }
    }
}

Human Readable Output

Domain List

domain tags
foobar.com mal

file


Returns File information and reputation.

Base Command

file

Input

Argument Name Description Required
file List of File Hashes (accepts MD5, SHA1, SHA256, SHA512). Required

Context Output

Path Type Description
Synapse.File.hash String The queried file hash.
Synapse.File.MD5 String The MD5 hash of the file.
Synapse.File.SHA1 String The SHA1 hash of the file.
Synapse.File.SHA256 String The SHA256 hash of the file.
Synapse.File.SHA512 String The SHA256 hash of the file.
Synapse.File.query String The formatted query in storm syntax.
File.MD5 String The MD5 hash of the file.
File.SHA1 String The SHA1 hash of the file.
File.SHA256 String The SHA256 hash of the file.
File.SHA512 String The SHA512 hash of the file.
File.Malicious.Vendor String For malicious files, the vendor that made the decision.
File.Malicious.Description String For malicious files, the full description.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Type String The indicator type.
DBotScore.Score Number The actual score.
DBotScore.Vendor String The vendor used to calculate the score.

Command Example

!file file="9e0c442ee3157d3f3aa2be30a1d24d81"

Context Example

{
    "DBotScore": {
        "Indicator": "9e0c442ee3157d3f3aa2be30a1d24d81",
        "Score": 3,
        "Type": "file",
        "Vendor": "Synapse"
    },
    "File": {
        "MD5": "9e0c442ee3157d3f3aa2be30a1d24d81",
        "Malicious": {
            "Description": "Synapse returned reputation tag: mal",
            "Vendor": "Synapse"
        },
        "SHA1": "e7b03ed4dbdfb79477c49942d5796d3dfc78ac7e",
        "SHA256": "290f64a315850c5bccc907f79cbeabd79345719df738ee5d02dc3447d04675b3",
        "SHA512": "53e6baa124f54462786f1122e98e38ff1be3de82fe2a96b1849a8637043fd847eec7e0f53307bddf7a066565292d500c36c941f1f3bb9dcac807b2f4a0bfce1b"
    },
    "Synapse": {
        "File": {
            "MD5": "9e0c442ee3157d3f3aa2be30a1d24d81",
            "SHA1": "e7b03ed4dbdfb79477c49942d5796d3dfc78ac7e",
            "SHA256": "290f64a315850c5bccc907f79cbeabd79345719df738ee5d02dc3447d04675b3",
            "SHA512": "53e6baa124f54462786f1122e98e38ff1be3de82fe2a96b1849a8637043fd847eec7e0f53307bddf7a066565292d500c36c941f1f3bb9dcac807b2f4a0bfce1b",
            "hash": "9e0c442ee3157d3f3aa2be30a1d24d81",
            "query": "file:bytes:md5=9e0c442ee3157d3f3aa2be30a1d24d81",
            "tags": [
                "mal"
            ]
        }
    }
}

Human Readable Output

File List

MD5 SHA1 SHA256 SHA512 hash query tags
9e0c442ee3157d3f3aa2be30a1d24d81 e7b03ed4dbdfb79477c49942d5796d3dfc78ac7e 290f64a315850c5bccc907f79cbeabd79345719df738ee5d02dc3447d04675b3 53e6baa124f54462786f1122e98e38ff1be3de82fe2a96b1849a8637043fd847eec7e0f53307bddf7a066565292d500c36c941f1f3bb9dcac807b2f4a0bfce1b 9e0c442ee3157d3f3aa2be30a1d24d81 file:bytes:md5=9e0c442ee3157d3f3aa2be30a1d24d81 mal

synapse-storm-query


Execute a Synapse Storm query.

Base Command

synapse-storm-query

Input

Argument Name Description Required
query Synapse storm query (i.e. “inet:ipv4=1.2.3.4”) Required
limit Limit the number of results returned. Default is 100. Optional

Context Output

Path Type Description
Synapse.Nodes.created String Timestamp when the node was first created in the Synapse Cortex.
Synapse.Nodes.form String The type of node (i.e. “inet:ipv4” for an IP address).
Synapse.Nodes.tags String The tags associated with the resulting node.
Synapse.Nodes.valu String The node primary value (i.e. “1.2.3.4” for an IP).

Command Example

!synapse-storm-query query="inet:ipv4=1.2.3.5" limit=1

Context Example

{
    "Synapse": {
        "Nodes": {
            "created": "2020/09/12 10:07:17 EDT",
            "form": "inet:ipv4",
            "tags": [
                "test.foo",
                "test.testing"
            ],
            "valu": "1.2.3.5"
        }
    }
}

Human Readable Output

Synapse Query Results: inet:ipv4=1.2.3.5

form valu created tags
inet:ipv4 1.2.3.5 2020/09/12 10:07:17 EDT test.foo,
test.testing

Synapse Node Properties

.created type
1599919637048 unicast

synapse-list-users


Lists current users in Synapse Cortex.

Base Command

synapse-list-users

Input

There are no input arguments for this command.

Context Output

Path Type Description
Synapse.Users.Admin Boolean True/False whether the Synapse user is an admin.
Synapse.Users.Email String The email address of the Synapse user.
Synapse.Users.Iden String The unique identifier of the Synapse user.
Synapse.Users.Name String The user’s Synapse username.
Synapse.Users.Roles String The roles applied to the Synapse user.
Synapse.Users.Rules String The rules applied to the Synapse user.

Command Example


#### Context Example

```json
{
    "Synapse": {
        "Users": [
            {
                "Admin": true,
                "Email": null,
                "Iden": "9e4fe25a281f3f65aff2fa192d54c705",
                "Name": "root",
                "Roles": [],
                "Rules": []
            },
            {
                "Admin": false,
                "Email": null,
                "Iden": "a2bfead4c16b0354af2a92aa05588fc9",
                "Name": "testuser",
                "Roles": [
                    "xsoar-role",
                    "all"
                ],
                "Rules": []
            },
            {
                "Admin": false,
                "Email": null,
                "Iden": "eec037c730f0976a1b742b9f9773a52e",
                "Name": "xsoartesting",
                "Roles": [
                    "all"
                ],
                "Rules": []
            }
        ]
    }
}

Human Readable Output

Synapse Users

Name Email Admin Rules Roles
root   true    
testuser   false   xsoar-role,
all
xsoartesting   false   all

synapse-list-roles


Lists current roles in Synapse Cortex.

Base Command

synapse-list-roles

Input

There are no input arguments for this command.

Context Output

Path Type Description
Synapse.Roles.Iden String The unique identifier of the Synapse Role.
Synapse.Roles.Name String The name of the Synapse Role.
Synapse.Roles.Rules String The rules applied to the Synapse Role.

Command Example


#### Context Example

```json
{
    "Synapse": {
        "Roles": [
            {
                "Iden": "bcf176a4cbe240ae1dcf9fbebdffa680",
                "Name": "xsoar-role",
                "Rules": []
            },
            {
                "Iden": "c486fa9eb8d50a8c35a60687f12dc4c9",
                "Name": "all",
                "Rules": []
            },
            {
                "Iden": "e7e6ee238bc5bceeff96d10f100142ae",
                "Name": "xsoartestingrole",
                "Rules": []
            }
        ]
    }
}

Human Readable Output

Synapse Roles

Name Iden Rules
xsoar-role bcf176a4cbe240ae1dcf9fbebdffa680  
all c486fa9eb8d50a8c35a60687f12dc4c9  
xsoartestingrole e7e6ee238bc5bceeff96d10f100142ae  

synapse-create-user


Create a new Synapse user.

Base Command

synapse-create-user

Input

Argument Name Description Required
username New username to be created. Required
password Optionally set the new user’s password. Optional

Context Output

Path Type Description
Synapse.Users.Admin Boolean True/False whether the Synapse user is an admin.
Synapse.Users.Email String The email address of the Synapse user.
Synapse.Users.Iden String The unique identifier of the Synapse user.
Synapse.Users.Name String The user’s Synapse username.
Synapse.Users.Roles String The roles applied to the Synapse user.
Synapse.Users.Rules String The rules applied to the Synapse user.

Command Example

!synapse-create-user username="xsoardemo" password="secret"

Context Example

{
    "Synapse": {
        "Users": {
            "Admin": false,
            "Email": null,
            "Iden": "f1ac5126df0e7407a0804fc6bd41534d",
            "Name": "xsoardemo",
            "Roles": [
                "all"
            ],
            "Rules": []
        }
    }
}

Human Readable Output

Synapse New User

Name Email Admin Rules Roles
xsoardemo   false   all

synapse-create-role


Create a new Synapse role.

Base Command

synapse-create-role

Input

Argument Name Description Required
role New role to create in Synapse. Required

Context Output

Path Type Description
Synapse.Roles.Iden String The unique identifier of the Synapse Role.
Synapse.Roles.Name String The name of the Synapse Role.
Synapse.Roles.Rules String The rules applied to the Synapse Role.

Command Example

!synapse-create-role role="xsoar-role-demo"

Context Example

{
    "Synapse": {
        "Roles": {
            "Iden": "029019964000fef6ccd2be428f496423",
            "Name": "xsoar-role-demo",
            "Rules": []
        }
    }
}

Human Readable Output

Synapse New Role

Name Iden Rules
xsoar-role-demo 029019964000fef6ccd2be428f496423  

synapse-grant-user-role


Grants a user access to role based perrmissions.

Base Command

synapse-grant-user-role

Input

Argument Name Description Required
user User’s “iden” property - not the username. Required
role Role’s “iden” property - not the name of the role. Required

Context Output

Path Type Description
Synapse.Users.Admin Boolean True/False whether the Synapse user is an admin.
Synapse.Users.Email String The email address of the Synapse user.
Synapse.Users.Iden String The unique identifier of the Synapse user.
Synapse.Users.Name String The user’s Synapse username.
Synapse.Users.Roles String The roles applied to the Synapse user.
Synapse.Users.Rules String The rules applied to the Synapse user.

Command Example

!synapse-grant-user-role user="a2bfead4c16b0354af2a92aa05588fc9" role="bcf176a4cbe240ae1dcf9fbebdffa680"

Context Example

{
    "Synapse": {
        "Users": {
            "Admin": false,
            "Email": null,
            "Iden": "a2bfead4c16b0354af2a92aa05588fc9",
            "Name": "testuser",
            "Roles": [
                "xsoar-role",
                "all"
            ],
            "Rules": []
        }
    }
}

Human Readable Output

Synapse New User Role

Name Email Admin Rules Roles
testuser   false   xsoar-role,
all

synapse-query-model


Query the Synapse data model and return details for given type or form (i.e. “inet:ipv4” for an IPv4 IP address).

Base Command

synapse-query-model

Input

Argument Name Description Required
query Type/Form query (i.e. “inet:ipv4” or “inet”fqdn”) Required

Context Output

Path Type Description
Synapse.Model.Doc String The docstring associated with the particular Synapse model element.
Synapse.Model.Example String An example of the given Synapse element.
Synapse.Model.Form String A form is the definition of an object in the Synapse data model (node).
Synapse.Model.Properties String The unique properties associated with the given Synapse object.
Synapse.Model.Type String A Type is the definition of a data element within the data model.
Synapse.Model.Valu String The given value of the Synapse object type.

Command Example

!synapse-query-model query="file:bytes"

Context Example

{
    "Synapse": {
        "Model": {
            "Doc": "The file bytes type with SHA256 based primary property.",
            "Example": "N/A",
            "Form": "file:bytes",
            "Properties": {
                ".created": "The time the node was created in the cortex.",
                ".seen": "The time interval for first/last observation of the node.",
                "md5": "The md5 hash of the file.",
                "mime": "The \"best\" mime type name for the file.",
                "mime:pe:compiled": "The compile time of the file according to the PE header.",
                "mime:pe:exports:libname": "The export library name according to the PE.",
                "mime:pe:exports:time": "The export time of the file according to the PE.",
                "mime:pe:imphash": "The PE import hash of the file as calculated by pefile; https://github.com/erocarrera/pefile .",
                "mime:pe:pdbpath": "The PDB string according to the PE.",
                "mime:pe:richhdr": "The sha256 hash of the rich header bytes.",
                "mime:pe:size": "The size of the executable file according to the PE file header.",
                "mime:x509:cn": "The Common Name (CN) attribute of the x509 Subject.",
                "name": "The best known base name for the file.",
                "sha1": "The sha1 hash of the file.",
                "sha256": "The sha256 hash of the file.",
                "sha512": "The sha512 hash of the file.",
                "size": "The file size in bytes."
            },
            "Type": "file:bytes",
            "Valu": "file:bytes"
        }
    }
}

Human Readable Output

Synapse Model Type

Type Doc Example
file:bytes The file bytes type with SHA256 based primary property. N/A

Synapse file:bytes Form Properties

.seen .created size md5 sha1 sha256 sha512 name mime mime:x509:cn mime:pe:size mime:pe:imphash mime:pe:compiled mime:pe:pdbpath mime:pe:exports:time mime:pe:exports:libname mime:pe:richhdr
The time interval for first/last observation of the node. The time the node was created in the cortex. The file size in bytes. The md5 hash of the file. The sha1 hash of the file. The sha256 hash of the file. The sha512 hash of the file. The best known base name for the file. The “best” mime type name for the file. The Common Name (CN) attribute of the x509 Subject. The size of the executable file according to the PE file header. The PE import hash of the file as calculated by pefile; https://github.com/erocarrera/pefile . The compile time of the file according to the PE header. The PDB string according to the PE. The export time of the file according to the PE. The export library name according to the PE. The sha256 hash of the rich header bytes.

Configuration parameters

  • url — Server URL (e.g. https://synapse.vertex.link) (required)
  • port — REST API Port (default is 4443). (required)
  • credentials — Username and password to user to authenticate to Synapse. (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • timezone — Timezone (optional)
  • bad_tag — Malicious Tag
  • good_tag — Benign Tag
  • use_optic — Use Synapse Optic
  • integrationReliability — Source Reliability
  • feedExpirationPolicy
  • feedExpirationInterval

Commands (11)

  • domain

    Returns Domain information and reputation.

  • file

    Returns File information and reputation.

  • ip

    Returns IP information and reputation.

  • synapse-create-role

    Create a new Synapse role.

  • synapse-create-user

    Create a new Synapse user.

  • synapse-grant-user-role

    Grants a user access to role based perrmissions.

  • synapse-list-roles

    Lists current roles in Synapse Cortex.

  • synapse-list-users

    Lists current users in Synapse Cortex.

  • synapse-query-model

    Query the Synapse data model and return details for given type or form (i.e. "inet:ipv4" for an IPv4 IP address).

  • synapse-storm-query

    Execute a Synapse Storm query.

  • url

    Returns URL information and reputation.

import asyncio
import ipaddress
import json

import aiohttp
import pytz
import urllib3
from aiohttp import TCPConnector
from CommonServerPython import *

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

DATE_FORMAT = "%Y/%m/%d %H:%M:%S %Z"
TIMEZONE = demisto.params().get("timezone", None)

""" CLIENT """


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

    def __init__(self, base_url, username, password, proxy, **kwargs):
        self._base_url = base_url
        self.username = username
        self.password = password
        self.proxy = proxy
        self.auth = (username, password)
        self.aio_auth = aiohttp.BasicAuth(username, password)
        super().__init__(base_url, **kwargs)

    def _check_for_error(self, resp):
        """
        Checks for custom synapse error message in JSON response.
        Raises error if so.
        """
        if resp.get("status") != "ok":
            code = resp.get("code")
            mesg = resp.get("mesg")
            raise Exception(f"Synapse API Error: ({code}): {mesg}")

    def login(self):
        """
        Login to Synapse and validate credentials.
        """

        resp = self._http_request(method="POST", url_suffix="/login", json_data={"user": self.username, "passwd": self.password})

        self._check_for_error(resp)

        return resp

    def add_user(self, new_user, new_passwd):
        """
        Adds a new user to Synapse with given password.
        """

        resp = self._http_request(
            method="POST", url_suffix="/auth/adduser", json_data={"name": new_user, "passwd": new_passwd}, auth=self.auth
        )

        self._check_for_error(resp)

        return resp

    def add_role(self, new_role):
        """
        Adds a new user role to Synapse.
        """

        resp = self._http_request(method="POST", url_suffix="/auth/addrole", json_data={"name": new_role}, auth=self.auth)

        self._check_for_error(resp)

        return resp

    def grant_role(self, user, role):
        """
        Adds a new user role to Synapse.
        """

        resp = self._http_request(method="POST", url_suffix="/auth/grant", json_data={"user": user, "role": role}, auth=self.auth)

        self._check_for_error(resp)

        return resp

    def list_users(self):
        """
        Collects all users in Synapse.
        """

        resp = self._http_request(method="GET", url_suffix="/auth/users", auth=self.auth)

        self._check_for_error(resp)

        return resp

    def list_roles(self):
        """
        Collects all user roles in Synapse.
        """

        resp = self._http_request(method="GET", url_suffix="/auth/roles", auth=self.auth)

        self._check_for_error(resp)

        return resp

    def get_model(self):
        """
        Queries and returns full model json.
        """

        resp = self._http_request(method="GET", url_suffix="/model", auth=self.auth)

        self._check_for_error(resp)

        return resp

    async def synapse_get_nodes(self, data, limit=100):
        """
        Async function to query for Synapse nodes via storm.
        """
        my_query = f"{data} | limit {limit}"
        nodes = []  # Nodes to return
        async with aiohttp.ClientSession(connector=TCPConnector(ssl=self._verify), trust_env=self.proxy) as sess:
            address = urljoin(self._base_url, "/storm")
            query = {"query": my_query}
            async with sess.get(address, json=query, auth=self.aio_auth) as resp:
                async for byts, _x in resp.content.iter_chunks():
                    if not byts:
                        break
                    mesg = json.loads(byts)
                    if mesg[0] == "node":
                        nodes.append(mesg[1])
        return nodes


""" HELPER FUNCTIONS """


def validate_timezone_helper(TIMEZONE):
    """
    Validates Timezone format is correct before assuming it is.
    """
    if TIMEZONE not in pytz.all_timezones:
        return_error(f'Error: Timezone format "{TIMEZONE}" invalid')
    else:
        tz = pytz.timezone(TIMEZONE)
    return tz  # pylint: disable=E0606


def convert_raw_into_nodes_helper(results):
    """
    Accepts raw node json and returns formatted list of dicts (nodes).
    """

    nodes = []
    for item in results:
        t_stamp = convert_epoch_timestamp_helper(item[1]["props"].get(".created"))
        node = {"form": item[0][0], "created": t_stamp, "tags": get_full_tags_helper(item[1].get("tags"))}
        if item[0][0] == "inet:ipv4":
            node["valu"] = ipaddress.ip_address(item[0][1]).__str__()
        else:
            node["valu"] = item[0][1]
        nodes.append(node)
    return nodes


def get_full_tags_helper(data):
    """
    Accepts raw REST Response for tags key and returns list of longest heirarchical tags.
    """
    tags = []

    temp_tags = list(data.keys())
    if temp_tags:
        tags.append(temp_tags.pop(0))
    else:
        return tags

    for _i in range(len(temp_tags)):
        if temp_tags:
            temp = temp_tags.pop(0)
        else:
            break
        for tag in tags:
            if temp in tag:
                continue
            elif tag in temp:
                tags.remove(tag)
                tags.append(temp)
            else:
                tags.append(temp)
    return tags


def convert_epoch_timestamp_helper(timestamp):
    """
    Accepts Epoch timestamp and localizes to UTC (timestamps per Synapse are all in UTC).
    """

    raw_date = datetime.fromtimestamp(float(timestamp) / 1000.0)
    utc_date = raw_date.replace(tzinfo=pytz.UTC)

    if TIMEZONE:
        tz = validate_timezone_helper(TIMEZONE)
        current_date = utc_date.astimezone(tz)
        return current_date.strftime(DATE_FORMAT)
    else:
        return utc_date.strftime(DATE_FORMAT)


def model_query_helper(model, query):
    """
    Accepts model (full json) and a node type (str) to query.
    Returns properties for given node type. Raises error not found if not present.
    """
    parsed_data = {"query": query}
    mod_types = list(model["types"].keys())
    mod_forms = list(model["forms"].keys())

    if (query not in mod_types) and (query not in mod_forms):
        raise Exception(f'Error: Query "{query}" not found in model. Try adjusting syntax (i.e. "inet:ipv4").')

    parsed_data["type"] = model["types"].get(query)

    if query in mod_forms:
        parsed_data["form"] = model["forms"].get(query)

    return parsed_data


def model_query_properties_helper(form):
    """
    Accepts JSON of cortex form and returns formatted properties.
    """
    data = {}
    for prop, valu in form.get("props").items():
        data[prop] = valu.get("doc", "N/A")

    return data


def user_roles_helper(client, roles):
    """
    Accepts list of role identifiers and converts them to named roles. Return list of names.
    """
    data = client.list_roles()
    named_roles = []

    for role in data.get("result"):
        if role.get("iden") in roles:
            named_roles.append(role.get("name"))

    return named_roles


def user_rules_helper(rules):
    """
    Accepts REST rules response and returns list of joined rules.
    """
    new_rules = []
    if not rules:
        return rules
    for rule in rules:
        if rule[0]:
            new_rules.append(".".join(rule[1]))
    return new_rules


def file_regex_helper(hash):
    """
    Accepts a raw hash and regex matches to determine what type.
    """

    if re.match(md5Regex, hash):
        file_query = f"file:bytes:md5={hash}"
    elif re.match(sha1Regex, hash):
        file_query = f"file:bytes:sha1={hash}"
    elif re.match(sha256Regex, hash):
        file_query = f"file:bytes:sha256={hash}"
    elif re.match(sha512Regex, hash):
        file_query = f"file:bytes:sha512={hash}"
    else:
        raise ValueError(f'Value "{hash}" is not a valid File Hash.')

    return file_query


def file_context_builder_helper(file, data):
    """
    Accepts hash and parsed query response. Adds MD5, SHA1, SHA256, SHA512.
    """
    file_context = {"hash": file, "tags": get_full_tags_helper(data[0][1].get("tags"))}
    if data[0][1]["props"].get("md5"):
        file_context["MD5"] = data[0][1]["props"].get("md5")
    if data[0][1]["props"].get("sha1"):
        file_context["SHA1"] = data[0][1]["props"].get("sha1")
    if data[0][1]["props"].get("sha256"):
        file_context["SHA256"] = data[0][1]["props"].get("sha256")
    if data[0][1]["props"].get("sha512"):
        file_context["SHA512"] = data[0][1]["props"].get("sha512")

    return file_context


""" COMMAND FUNCTIONS """


def test_module(client):
    """
    Tests API connectivity and authentication'
    """
    client.login()
    return "ok"


def ip_reputation_command(client, args, good_tag, bad_tag) -> List[CommandResults]:
    """
    Returns IP Reputation for a list of IPs.
    """
    ips = argToList(args.get("ip"))
    if len(ips) == 0:
        raise ValueError("IP(s) not specified")

    for ip in ips:
        if not re.match(ipv4Regex, ip):
            raise ValueError(f'Value "{ip}" is not a valid IP address.')

    command_results: List[CommandResults] = []
    for ip in ips:
        query = f"inet:ipv4={ip}"
        data = asyncio.run(client.synapse_get_nodes(query))
        if not data:
            continue
        ip_data = {"ip": ip, "tags": get_full_tags_helper(data[0][1].get("tags"))}

        score = Common.DBotScore.NONE  # unknown
        reputation = {"tag": "N/A"}
        if bad_tag in ip_data["tags"]:
            score = Common.DBotScore.BAD  # bad
            reputation["tag"] = bad_tag
        elif good_tag in ip_data["tags"]:
            score = Common.DBotScore.GOOD  # good
            reputation["tag"] = good_tag

        # Create the DBotScore structure first using the Common.DBotScore class.
        dbot_score = Common.DBotScore(
            indicator=ip,
            indicator_type=DBotScoreType.IP,
            integration_name="Synapse",
            score=score,
            malicious_description=f'Synapse returned reputation tag: {reputation["tag"]}',
            reliability=demisto.params().get("integrationReliability"),
        )

        # Create the IP Standard Context structure using Common.IP and add
        # dbot_score to it.
        ip_standard_context = Common.IP(ip=ip, dbot_score=dbot_score)

        command_results.append(
            CommandResults(
                readable_output=tableToMarkdown("IP Details", ip_data),
                outputs_prefix="Synapse.IP",
                outputs_key_field="ip",
                outputs=ip_data,
                indicator=ip_standard_context,
            )
        )

    return command_results


def domain_reputation_command(client, args, good_tag, bad_tag) -> List[CommandResults]:
    """
    Returns Domain Reputation for a list of Domains.
    """
    domains = argToList(args.get("domain"))
    if len(domains) == 0:
        raise ValueError("Domain(s) not specified")

    command_results: List[CommandResults] = []
    for domain in domains:
        query = f"inet:fqdn={domain}"
        data = asyncio.run(client.synapse_get_nodes(query))
        if not data:
            continue
        domain_data = {"domain": domain, "tags": get_full_tags_helper(data[0][1].get("tags"))}

        score = Common.DBotScore.NONE  # unknown
        reputation = {"tag": "N/A"}
        if bad_tag in domain_data["tags"]:
            score = Common.DBotScore.BAD  # bad
            reputation["tag"] = bad_tag
        elif good_tag in domain_data["tags"]:
            score = Common.DBotScore.GOOD  # good
            reputation["tag"] = good_tag

        # Create the DBotScore structure first using the Common.DBotScore class.
        dbot_score = Common.DBotScore(
            indicator=domain,
            indicator_type=DBotScoreType.DOMAIN,
            integration_name="Synapse",
            score=score,
            malicious_description=f'Synapse returned reputation tag: {reputation["tag"]}',
            reliability=demisto.params().get("integrationReliability"),
        )

        # Create the Domain Standard Context structure using Common.Domain and add
        # dbot_score to it.
        domain_standard_context = Common.Domain(domain=domain, dbot_score=dbot_score)

        command_results.append(
            CommandResults(
                readable_output=tableToMarkdown("Domain Details", domain_data),
                outputs_prefix="Synapse.Domain",
                outputs_key_field="domain",
                outputs=domain_data,
                indicator=domain_standard_context,
            )
        )

    return command_results


def url_reputation_command(client, args, good_tag, bad_tag) -> List[CommandResults]:
    """
    Returns URL Reputation for a list of URLs.
    """
    urls = argToList(args.get("url"))
    if len(urls) == 0:
        raise ValueError("URL(s) not specified")

    for url in urls:
        if not re.match(urlRegex, url):
            raise ValueError(f'Value "{url}" is not a valid URL address.')

    command_results: List[CommandResults] = []
    for url in urls:
        query = f"inet:url={url}"
        data = asyncio.run(client.synapse_get_nodes(query))
        if not data:
            continue
        url_data = {"url": url, "tags": get_full_tags_helper(data[0][1].get("tags"))}

        score = Common.DBotScore.NONE  # unknown
        reputation = {"tag": "N/A"}
        if bad_tag in url_data["tags"]:
            score = Common.DBotScore.BAD  # bad
            reputation["tag"] = bad_tag
        elif good_tag in url_data["tags"]:
            score = Common.DBotScore.GOOD  # good
            reputation["tag"] = good_tag

        # Create the DBotScore structure first using the Common.DBotScore class.
        dbot_score = Common.DBotScore(
            indicator=url,
            indicator_type=DBotScoreType.URL,
            integration_name="Synapse",
            score=score,
            malicious_description=f'Synapse returned reputation tag: {reputation["tag"]}',
            reliability=demisto.params().get("integrationReliability"),
        )

        # Create the URL Standard Context structure using Common.URL and add
        # dbot_score to it.
        url_standard_context = Common.URL(url=url, dbot_score=dbot_score)

        command_results.append(
            CommandResults(
                readable_output=tableToMarkdown("URL Details", url_data),
                outputs_prefix="Synapse.URL",
                outputs_key_field="url",
                outputs=url_data,
                indicator=url_standard_context,
            )
        )

    return command_results


def file_reputation_command(client, args, good_tag, bad_tag) -> List[CommandResults]:
    """
    Returns File Reputation for a list of hashes (MD5, SHA1, or SHA256).
    """
    files = argToList(args.get("file"))
    if len(files) == 0:
        raise ValueError("File(s) not specified")

    command_results: List[CommandResults] = []
    for file in files:
        file_query = file_regex_helper(file)
        data = asyncio.run(client.synapse_get_nodes(file_query))
        if not data:
            continue
        file_data = file_context_builder_helper(file, data)
        file_data["query"] = file_query

        score = Common.DBotScore.NONE  # unknown
        reputation = {"tag": "N/A"}
        if bad_tag in file_data["tags"]:
            score = Common.DBotScore.BAD  # bad
            reputation["tag"] = bad_tag
        elif good_tag in file_data["tags"]:
            score = Common.DBotScore.GOOD  # good
            reputation["tag"] = good_tag

        # Create the DBotScore structure first using the Common.DBotScore class.
        dbot_score = Common.DBotScore(
            indicator=file,
            indicator_type=DBotScoreType.FILE,
            integration_name="Synapse",
            score=score,
            malicious_description=f'Synapse returned reputation tag: {reputation["tag"]}',
            reliability=demisto.params().get("integrationReliability"),
        )

        # Create the File Standard Context structure using Common.File and add
        # dbot_score to it.
        file_standard_context = Common.File(
            md5=file_data.get("MD5"),
            sha1=file_data.get("SHA1"),
            sha256=file_data.get("SHA256"),
            sha512=file_data.get("SHA512"),
            dbot_score=dbot_score,
        )

        command_results.append(
            CommandResults(
                readable_output=tableToMarkdown("File Details", file_data),
                outputs_prefix="Synapse.File",
                outputs_key_field="hash",
                outputs=file_data,
                indicator=file_standard_context,
            )
        )

    return command_results


def storm_query_command(client, args):
    """
    Executes a storm query and expects nodes in response.
    """
    try:
        data = asyncio.run(client.synapse_get_nodes(args.get("query"), args.get("limit")))
    except DemistoException as e:
        if "AuthDeny" in str(e):
            return "Authorization Error: make sure credentials are correct set"
        else:
            raise e
    else:
        nodes = convert_raw_into_nodes_helper(data)

    name = f'Synapse Query Results: `{args.get("query")}`'
    headers = ["form", "valu", "created", "tags"]
    readable_output = tableToMarkdown(name, nodes, headers=headers, removeNull=True)

    if len(nodes) == 1:
        name_single = "Synapse Node Properties"
        headers_single = list(data[0][1]["props"].keys())
        readable_output += tableToMarkdown(name_single, data[0][1]["props"], headers=headers_single, removeNull=False)

    results = CommandResults(
        outputs_prefix="Synapse.Nodes",
        outputs_key_field="valu",
        outputs=nodes,
        readable_output=readable_output,
        raw_response=data,
    )

    return results


def list_users_command(client):
    """
    Executes API call for list users and returns response.
    """

    data = client.list_users()
    users = []

    for user in data.get("result"):
        my_user = {
            "Name": user.get("name"),
            "Email": user.get("email"),
            "Admin": user.get("admin"),
            "Iden": user.get("iden"),
            "Rules": user_rules_helper(user.get("rules")),
            "Roles": user_roles_helper(client, user.get("roles")),
        }
        users.append(my_user)

    name = "Synapse Users"
    headers = ["Name", "Email", "Admin", "Rules", "Roles"]
    readable_output = tableToMarkdown(name, users, headers=headers, removeNull=False)

    results = CommandResults(
        outputs_prefix="Synapse.Users",
        outputs_key_field="Iden",
        outputs=users,
        readable_output=readable_output,
        raw_response=data,
    )

    return results


def list_roles_command(client):
    """
    Executes API call for list roles and returns response.
    """

    data = client.list_roles()
    roles = []

    for role in data.get("result"):
        my_role = {"Name": role.get("name"), "Iden": role.get("iden"), "Rules": user_rules_helper(role.get("rules"))}
        roles.append(my_role)

    name = "Synapse Roles"
    headers = ["Name", "Iden", "Rules"]
    readable_output = tableToMarkdown(name, roles, headers=headers, removeNull=False)

    results = CommandResults(
        outputs_prefix="Synapse.Roles",
        outputs_key_field="Iden",
        outputs=roles,
        readable_output=readable_output,
        raw_response=data,
    )

    return results


def add_user_command(client, args):
    """
    Add a user to Synapse. Accepts username and password.
    """

    data = client.add_user(args.get("username"), args.get("password"))
    user = data.get("result")

    my_user = {
        "Name": user.get("name"),
        "Email": user.get("email"),
        "Admin": user.get("admin"),
        "Iden": user.get("iden"),
        "Rules": user_rules_helper(user.get("rules")),
        "Roles": user_roles_helper(client, user.get("roles")),
    }

    name = "Synapse New User"
    headers = ["Name", "Email", "Admin", "Rules", "Roles"]
    readable_output = tableToMarkdown(name, my_user, headers=headers, removeNull=False)

    results = CommandResults(
        outputs_prefix="Synapse.Users",
        outputs_key_field="Iden",
        outputs=my_user,
        readable_output=readable_output,
        raw_response=data,
    )

    return results


def add_role_command(client, args):
    """
    Add a role to Synapse. Accepts new role.
    """

    data = client.add_role(args.get("role"))
    role = data.get("result")

    my_role = {
        "Name": role.get("name"),
        "Iden": role.get("iden"),
        "Rules": user_rules_helper(role.get("rules")),
    }

    name = "Synapse New Role"
    headers = ["Name", "Iden", "Rules"]
    readable_output = tableToMarkdown(name, my_role, headers=headers, removeNull=False)

    results = CommandResults(
        outputs_prefix="Synapse.Roles",
        outputs_key_field="Iden",
        outputs=my_role,
        readable_output=readable_output,
        raw_response=data,
    )

    return results


def grant_user_role_command(client, args):
    """
    Grant role to a user.
    """

    data = client.grant_role(args.get("user"), args.get("role"))

    user = data.get("result")

    my_user = {
        "Name": user.get("name"),
        "Email": user.get("email"),
        "Admin": user.get("admin"),
        "Iden": user.get("iden"),
        "Rules": user_rules_helper(user.get("rules")),
        "Roles": user_roles_helper(client, user.get("roles")),
    }

    name = "Synapse New User Role"
    headers = ["Name", "Email", "Admin", "Rules", "Roles"]
    readable_output = tableToMarkdown(name, my_user, headers=headers, removeNull=False)

    results = CommandResults(
        outputs_prefix="Synapse.Users",
        outputs_key_field="Iden",
        outputs=my_user,
        readable_output=readable_output,
        raw_response=data,
    )

    return results


def query_model_command(client, args):
    """
    Accepts a node type (str) to query and returns docs and properties.
    """

    data = client.get_model()
    model_resp = model_query_helper(data.get("result"), args.get("query").lower())
    q_type, q_form = {}, {}

    if model_resp.get("type"):
        q_type = {
            "Type": model_resp.get("query"),
            "Doc": model_resp["type"]["info"].get("doc", "N/A"),
            "Example": model_resp["type"]["info"].get("ex", "N/A"),
        }
    if model_resp.get("form"):
        q_form = {"Form": model_resp.get("query"), "Properties": model_query_properties_helper(model_resp.get("form"))}
    full_resp = {"Valu": model_resp.get("query")}

    full_resp.update(q_type)
    full_resp.update(q_form)

    name = "Synapse Model Type"
    headers = ["Type", "Doc", "Example"]
    readable_output = tableToMarkdown(name, q_type, headers=headers, removeNull=False)

    if full_resp.get("Form"):
        name_form = f'Synapse `{full_resp.get("Valu")}` Form Properties'
        headers_form = list(q_form["Properties"].keys())
        readable_output += tableToMarkdown(name_form, q_form["Properties"], headers=headers_form, removeNull=False)

    results = CommandResults(
        outputs_prefix="Synapse.Model",
        outputs_key_field="Valu",
        outputs=full_resp,
        readable_output=readable_output,
        raw_response=model_resp,
    )

    return results


""" MAIN FUNCTION """


def main() -> None:
    """
    main function, parses params and runs command functions
    """
    username = demisto.params().get("credentials").get("identifier")
    password = demisto.params().get("credentials").get("password")
    port = demisto.params().get("port")
    base = demisto.params()["url"].rstrip("/") + ":" + str(port)
    use_ssl = not demisto.params().get("insecure", False)
    use_proxy = demisto.params().get("proxy", False)
    good_tag = demisto.params().get("good_tag")
    bad_tag = demisto.params().get("bad_tag")
    use_optic = demisto.params().get("use_optic", False)
    if use_optic:
        base_url = urljoin(base, "/api/v1/optic")
    else:
        base_url = urljoin(base, "/api/v1")

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

        if demisto.command() == "test-module":
            return_results(test_module(client))

        elif demisto.command() == "ip":
            return_results(ip_reputation_command(client, demisto.args(), good_tag, bad_tag))

        elif demisto.command() == "domain":
            return_results(domain_reputation_command(client, demisto.args(), good_tag, bad_tag))

        elif demisto.command() == "url":
            return_results(url_reputation_command(client, demisto.args(), good_tag, bad_tag))

        elif demisto.command() == "file":
            return_results(file_reputation_command(client, demisto.args(), good_tag, bad_tag))

        elif demisto.command() == "synapse-storm-query":
            return_results(storm_query_command(client, demisto.args()))

        elif demisto.command() == "synapse-list-users":
            return_results(list_users_command(client))

        elif demisto.command() == "synapse-list-roles":
            return_results(list_roles_command(client))

        elif demisto.command() == "synapse-create-user":
            return_results(add_user_command(client, demisto.args()))

        elif demisto.command() == "synapse-create-role":
            return_results(add_role_command(client, demisto.args()))

        elif demisto.command() == "synapse-grant-user-role":
            return_results(grant_user_role_command(client, demisto.args()))

        elif demisto.command() == "synapse-query-model":
            return_results(query_model_command(client, demisto.args()))

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


""" ENTRY POINT """

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