DB2

Integration to provide connectivity to IBM DB2 using the python ibm_db2 library.

Database · DB2

Details

IDDB2
ProviderIBM
CategoryDatabase
From Version6.0.0
Docker Imagedemisto/ibm-db2:1.0.0.9067966
Supported ModulesAgentix XSIAM

README

Integration to provide connectivity to IBM DB2 using the python ibm_db2 library.
This integration was integrated and tested with version 0.1 of DB2

Configure DB2 in Cortex

Parameter Required
Database host True
Port False
Database Name False
Username True
Password True
Connection Arguments (ex: arg1=val1&arg2=val2) False
Use an SSL connection False
Use Persistent Connection 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.

db2-query


Running a DB2 query command

Base Command

db2-query

Input

Argument Name Description Required
query The DB2 query to run. Required
limit The maximum number of results. Default is 50. Optional
skip The offset at which to start the result. Default is 0. Optional
bind_variables_name A comma separated list of names which will be replaced in query having ‘:<name>’. Optional
bind_variables_values A comma separated list of values corresponding to bind_variables_name or values in order of ‘?’ mark to be replaced in query. Optional

Context Output

There is no context output for this command.

Command Example

!db2-query query="CREATE TABLE publishers(publisher_id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(publisher_id))"

Human Readable Output

Configuration parameters

  • host — Database host (required)
  • port — Port
  • dbname — Database Name
  • credentials — Username (required)
  • connect_parameters — Connection Arguments (ex: arg1=val1&arg2=val2)
  • ssl_connect — Use an SSL connection
  • use_persistent — Use Persistent Connection

Commands (1)

  • db2-query

    Running a DB2 query command

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

"""Integration for DB2"""

import re
import traceback
from collections.abc import Callable
from typing import Any
from urllib.parse import parse_qsl

import ibm_db
import urllib3

# Disable insecure warnings
urllib3.disable_warnings()


""" CONSTANTS """

PROTOCOL = "TCPIP"
DRIVER_NAME = "{IBM DB2 ODBC DRIVER}"
CLEAN = re.compile(r"\[.*?\]")
COLON_REGEX = re.compile(r"(?<![:\w\x5c]):(\w+)(?!:)", re.UNICODE)

# ============== DB2 Connection Options ===============

OPTIONS = {
    "ATTR_CASE": ibm_db.ATTR_CASE,
    "SQL_ATTR_AUTOCOMMIT": ibm_db.SQL_ATTR_AUTOCOMMIT,
    "SQL_ATTR_INFO_USERID": ibm_db.SQL_ATTR_INFO_USERID,
    "SQL_ATTR_CURSOR_TYPE": ibm_db.SQL_ATTR_CURSOR_TYPE,
    "SQL_ATTR_INFO_ACCTSTR": ibm_db.SQL_ATTR_INFO_ACCTSTR,
    "SQL_ATTR_INFO_APPLNAME": ibm_db.SQL_ATTR_INFO_APPLNAME,
    "SQL_ATTR_CURRENT_SCHEMA": ibm_db.SQL_ATTR_CURRENT_SCHEMA,
    "SQL_ATTR_INFO_WRKSTNNAME": ibm_db.SQL_ATTR_INFO_WRKSTNNAME,
    "SQL_ATTR_INFO_PROGRAMNAME": ibm_db.SQL_ATTR_INFO_PROGRAMNAME,
    "SQL_ATTR_USE_TRUSTED_CONTEXT": ibm_db.SQL_ATTR_USE_TRUSTED_CONTEXT,
    "SQL_ATTR_TRUSTED_CONTEXT_USERID": ibm_db.SQL_ATTR_TRUSTED_CONTEXT_USERID,
    "SQL_ATTR_TRUSTED_CONTEXT_PASSWORD": ibm_db.SQL_ATTR_TRUSTED_CONTEXT_PASSWORD,
}
VALUES = {
    "SQL_ATTR_AUTOCOMMIT": {
        "SQL_AUTOCOMMIT_ON": ibm_db.SQL_AUTOCOMMIT_ON,
        "SQL_AUTOCOMMIT_OFF": ibm_db.SQL_AUTOCOMMIT_OFF,
    },
    "ATTR_CASE": {
        "CASE_NATURAL": ibm_db.CASE_NATURAL,
        "CASE_LOWER": ibm_db.CASE_LOWER,
        "CASE_UPPER": ibm_db.CASE_UPPER,
    },
    "SQL_ATTR_CURSOR_TYPE": {
        "SQL_CURSOR_FORWARD_ONLY": ibm_db.SQL_CURSOR_FORWARD_ONLY,
        "SQL_CURSOR_KEYSET_DRIVEN": ibm_db.SQL_CURSOR_KEYSET_DRIVEN,
        "SQL_CURSOR_DYNAMIC": ibm_db.SQL_CURSOR_DYNAMIC,
        "SQL_CURSOR_STATIC": ibm_db.SQL_CURSOR_STATIC,
    },
}


""" CLIENT CLASS """


class Client:
    """Client to use in the DB2 databases integration. Overrides BaseClient
    makes the connection to the DB2 DB Server
    """

    def __init__(
        self,
        host: str,
        username: str,
        password: str,
        port: str,
        database: str,
        ssl_connect: bool,
        connect_parameters: str = "",
        use_persistent=False,
    ):
        self.host = host
        self.username = username
        self.password = password
        self.port = port
        self.dbname = database
        self.connect_parameters = self._parse_connect_parameters(connect_parameters)
        self.ssl_connect = ssl_connect
        self.use_persistent = use_persistent
        self.connection = self._connect()

    @staticmethod
    def _parse_connect_parameters(connect_parameters: str) -> dict:
        """
        Parses a string of the form key1=value1&key2=value2 etc.
        into a dict with matching keys and values.

        Args:
            connect_parameters: The string with query parameters

        Returns:
            A dict with the keys and values.
        """

        connect_parameters_tuple_list = parse_qsl(connect_parameters, keep_blank_values=True)
        connect_parameters_dict = {}
        for key, value in connect_parameters_tuple_list:
            connect_parameters_dict[key] = value
        return connect_parameters_dict

    @property
    def create_url(self) -> str:
        """
        Create URL for making connection to given host

        Returns:
            string containing all of the required parameters
        """

        conn_string = (
            f"DRIVER={DRIVER_NAME};DATABASE={self.dbname};HOSTNAME={self.host};PORT"
            f"={self.port};PROTOCOL={PROTOCOL};UID={self.username};PWD={self.password};"
        )

        if self.ssl_connect:
            conn_string += "SECURITY=SSL;"

        return conn_string

    def _options(self) -> dict:
        """
        Map connection options with connection parameters
        `ibm_db.OPTION` will be called for every option
        """
        options = {}

        for key, val in self.connect_parameters.items():
            option: str = key.upper()
            value: str = val.upper()

            if option in OPTIONS and option in VALUES and value in VALUES[option]:
                options[option] = VALUES[option][value]
            elif option in OPTIONS and option not in VALUES:
                options[option] = val
            else:
                # skip options which are not valid
                pass

        return options

    def _connect(self) -> ibm_db.IBM_DBConnection:
        """
        Connecting to the host using required parameters and returning the `Connection` object

        Returns:
            a connection object that will be used in order to execute queries
        """

        conn: ibm_db.IBM_DBConnection = None

        try:
            if self.use_persistent:
                demisto.info("Initializing Persistent connection")
                conn = ibm_db.pconnect(self.create_url, "", "", conn_options=self._options())
            else:
                demisto.info("Initializing Non-Persistent connection")
                conn = ibm_db.connect(self.create_url, "", "", conn_options=self._options())

        except Exception:
            demisto.error(f"Connection State:\n{ibm_db.conn_error}")
            demisto.error(f"Connection Error:\n{ibm_db.conn_errormsg()}")
            raise DemistoException(f"DB2 Connection Failed:\n{ibm_db.conn_errormsg()}")

        return conn

    def _prepare_statement(self, query: str, bindvars: Any) -> Any:
        """
        Populate values from bindvars to query and `ibm_db.prepare` statement

        Args:
            query(str): db2 query string
            bindvars(Any): list/dictionay with values to populate query

        Returns:
            ibm_db.prepare
        """

        # Validate bindvars with respect to `?` or `:`
        if len(bindvars):
            if isinstance(bindvars, list) and query.count("?") < len(bindvars):
                raise DemistoException("Insufficient bind values found")
            elif isinstance(bindvars, dict) and query.count(":") < len(bindvars.keys()):
                raise DemistoException("Insufficient bind names & values found")

        demisto.info("Preparing Statement ...")

        if isinstance(bindvars, dict):
            try:

                def repl(x, bindvars=bindvars):
                    return f"'{bindvars[x.group(0).strip(':')]}'"

                query = re.sub(COLON_REGEX, repl=repl, string=query)
            except KeyError as err:
                demisto.error(f"{err.args[0]} key not found in bind names")
                raise DemistoException(f"{err.args[0]} key not found in bind names")

        stmt = ibm_db.prepare(self.connection, query)

        if isinstance(bindvars, list):
            for index, var in enumerate(bindvars, 1):
                ibm_db.bind_param(stmt, index, var)

        return stmt

    def execute_query(self, query: str, bind_vars: Any) -> tuple[list, list]:
        """
        Execute query at DB2 Database via connection

        Args:
            query(str): db2 query string
            bind_vars(Any): in case there are names and values - a bind_vars dict,
                            in case there are only values - list

        Returns:
            Tuple[results(List), headers(List)]
        """
        results = []
        headers = []
        status = False

        stmt = self._prepare_statement(query, bind_vars)

        try:
            demisto.info("Executing ...")
            status = ibm_db.execute(stmt)
            demisto.info("Done !!!")
        except Exception:
            demisto.error(clear(ibm_db.stmt_error()))
            demisto.error(clear(ibm_db.stmt_errormsg()))
            raise DemistoException(clear(ibm_db.stmt_errormsg()))

        demisto.info("Collecting results")
        if status:
            row = ibm_db.fetch_assoc(stmt)
            while row:
                results.append(row)
                row = ibm_db.fetch_assoc(stmt)

        if results:
            headers = [*results[0]]

        return results, headers

    def close(self) -> bool:
        demisto.info("Closing Connection")
        return ibm_db.close(self.connection)


""" HELPER FUNCTIONS """


def clear(message: str):
    """
    Clean data with square brackets from message

    Args:
        message(str): Any message string

    Returns:
        string with clean message
    """

    def repl(x):
        return ""

    return (re.sub(CLEAN, repl=repl, string=message)).strip()


def bind_variables(names: str, values: str) -> Any:
    """
    Binding of column names with their values or return list of values

    Args:
        names(str): column name to bind with values, must be in the length of the values list
        values(str): the values to bind with columns, can be in the length of the names list

    Returns:
        Any: a dict with column and value as a key value pair or list of values
    """
    names_list = argToList(names)
    values_list = argToList(values)

    # assuming the order of values is correct
    if values_list and not names_list:
        return list(values_list)
    elif len(names_list) == len(values_list):
        return dict(zip(names_list, values_list))
    else:
        raise Exception("The bind variables lists are not is the same length")


""" COMMAND FUNCTIONS """


def query_command(client: Client, args: dict, *_) -> CommandResults:
    """
    Executes the db2 query with the connection that was configured in the Client

    Args:
        client(Client): the client object with db connection
        args(demisto.args): arguments for the query-command
    """
    sql_query = str(args.get("query"))
    limit = int(args.get("limit", 50))
    skip = int(args.get("offset", 0))
    bind_variable_name = args.get("bind_variables_name", "")
    bind_variable_values = args.get("bind_variables_values", "")

    try:
        variables = bind_variables(bind_variable_name, bind_variable_values)
        result, headers = client.execute_query(sql_query, variables)

        converted_table = [dict(row) for row in result]
        table = [{str(key): str(value) for key, value in dictionary.items()} for dictionary in converted_table]
        table = table[skip : skip + limit]

        human_readable = tableToMarkdown(name="Query result:", t=table, headers=headers, removeNull=True)

        context = {"Result": table, "Query": sql_query, "DbName": f"{client.dbname}"}
        client.close()

        return CommandResults(
            outputs_prefix="DB2",
            outputs_key_field="Query",
            outputs=context,
            raw_response=result,
            readable_output=human_readable,
        )
    except Exception as err:
        client.close()
        demisto.error(f"error:\n {err}")
        if str(err).lower() == "column information cannot be retrieved: ":
            human_readable = f"{sql_query} Command Executed Successfully"
            return CommandResults(readable_output=human_readable)
        raise DemistoException(err)


def test_module(client: Client, *_) -> str:
    """
    If the connection in the client was successful the test will return OK
    if it wasn't an exception will be raised
    """
    return "ok"


def main():  # pragma: no cover
    """main function, parses params and runs command functions"""

    params = demisto.params()

    # Fetch required parameters
    host = params.get("host")
    uid = params.get("credentials").get("identifier")
    password = params.get("credentials").get("password")
    database = params.get("dbname")
    port = params.get("port", 50000)
    ssl_connect = params.get("ssl_connect")
    connect_params = params.get("connect_parameters")
    use_persistent = params.get("use_persistent")

    command = demisto.command()
    demisto.debug(f"command being called is {command}")

    try:
        client = Client(
            host=host,
            username=uid,
            password=password,
            port=port,
            database=database,
            ssl_connect=ssl_connect,
            connect_parameters=connect_params,
            use_persistent=use_persistent,
        )

        commands: dict[str, Callable] = {
            "test-module": test_module,
            "db2-query": query_command,
        }
        if command in commands:
            return_results(*commands[command](client, demisto.args(), command))
        else:
            raise NotImplementedError(f"{command} is not an existing DB2 command")
    except Exception as e:
        demisto.error(traceback.format_exc())  # print the traceback
        return_error(f"failed to execute {command} command.\nerror:\n{e!s}")


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