Publish List

The Publish List integration is used to publish XSOAR lists for external consumption.

Utilities · Publish List

Details

IDPublish List
ProviderOpen Source
CategoryUtilities
From Version6.0.0
Docker Imagedemisto/fastapi:0.125.0.10158186
Supported ModulesAgentix XSIAM

Configuration parameters

  • longRunning — Long running instance
  • longRunningPort — Listen Port (required)
  • credentials — Username
  • certificate — Certificate (Required for HTTPS)
  • key — Private Key (Required for HTTPS)
  • incidentType — Incident type
  • listsToPublish — Lists to Publish (required)
  • commaToLineBreak — List Items on Individual Lines
  • add_comment_if_empty — Add comment to empty list

Commands (0)

This integration defines no commands.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
import csv
import json
from copy import copy
from secrets import compare_digest
from tempfile import NamedTemporaryFile
from traceback import format_exc

import uvicorn
from fastapi import Depends, FastAPI, Request, Response
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.security.api_key import APIKey, APIKeyHeader
from uvicorn.logging import AccessFormatter

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

basic_auth = HTTPBasic(auto_error=False)
token_auth = APIKeyHeader(auto_error=False, name="Authorization")

listsToPublish = [x.strip() for x in demisto.params().get("listsToPublish").split(",")]
commaToLineBreak = demisto.params().get("commaToLineBreak")
commentIfEmpty = demisto.params().get("add_comment_if_empty")


def is_json(jstr):
    try:
        json.loads(jstr)
    except ValueError:
        return False
    return True


class PublishListAccessFormatter(AccessFormatter):
    def get_user_agent(self, scope: dict) -> str:
        headers = scope.get("headers", [])
        user_agent_header = list(filter(lambda header: header[0].decode() == "user-agent", headers))
        user_agent = ""
        if len(user_agent_header) == 1:
            user_agent = user_agent_header[0][1].decode()
        return user_agent

    def formatMessage(self, record):
        recordcopy = copy(record)
        scope = recordcopy.__dict__["scope"]
        user_agent = self.get_user_agent(scope)
        recordcopy.__dict__.update({"user_agent": user_agent})
        return super().formatMessage(recordcopy)


@app.get("/{xsoar_list}")
async def handle_get(
    xsoar_list: str,
    request: Request,
    credentials: HTTPBasicCredentials = Depends(basic_auth),
    token: APIKey = Depends(token_auth),
):
    credentials_param = demisto.params().get("credentials")
    if credentials_param and (username := credentials_param.get("identifier")):
        password = credentials_param.get("password", "")
        auth_failed = False
        header_name = None
        if username.startswith("_header"):
            header_name = username.split(":")[1]
            token_auth.model.name = header_name
            if not token or not compare_digest(token, password):
                auth_failed = True
        elif (not credentials) or (
            not (compare_digest(credentials.username, username) and compare_digest(credentials.password, password))
        ):
            auth_failed = True
        if auth_failed:
            request_headers = dict(request.headers)
            secret_header = (header_name or "Authorization").lower()
            if secret_header in request_headers:
                request_headers[secret_header] = "***"
            demisto.debug(f"Authorization failed - request headers {request_headers}")
            return Response(status_code=401, content="Authorization failed.")

    if xsoar_list not in listsToPublish:
        # Return list not found instead of permission denied to prevent brute force list name discovery
        return Response(status_code=404, content="List not found")

    try:
        list_response = demisto.internalHttpRequest("GET", f"/lists/download/{xsoar_list}")
        if len(list_response.get("body")) == 0 and commentIfEmpty:
            return Response(content="# Empty list")

        # For normal lists (not json) that are essentially just a long comma separated list
        # this puts each entry on a new line
        if commaToLineBreak and not is_json(list_response.get("body")):
            tmparr = []
            reader = csv.reader([list_response.get("body")], quoting=csv.QUOTE_MINIMAL)
            for row in reader:
                if type(row) is str:
                    r = str(row)  # new variable needed for mypy validation
                    tmparr.append(r)
                else:
                    for cell in row:
                        c = str(cell)  # new variable needed for mypy validation
                        tmparr.append(c)
            list_body = "\n".join(tmparr)
            return Response(content=list_body)

        return Response(content=list_response.get("body"))
    except Exception as e:
        return e


def main() -> None:
    try:
        try:
            port = int(demisto.params().get("longRunningPort"))
        except ValueError as e:
            raise ValueError(f"Invalid listen port - {e}")
        if demisto.command() == "test-module":
            return_results("ok")
        elif demisto.command() == "long-running-execution":
            while True:
                certificate = demisto.params().get("certificate", "")
                private_key = demisto.params().get("key", "")

                certificate_path = ""
                private_key_path = ""
                try:
                    ssl_args = {}

                    if certificate and private_key:
                        certificate_file = NamedTemporaryFile(delete=False)
                        certificate_path = certificate_file.name
                        certificate_file.write(bytes(certificate, "utf-8"))
                        certificate_file.close()
                        ssl_args["ssl_certfile"] = certificate_path

                        private_key_file = NamedTemporaryFile(delete=False)
                        private_key_path = private_key_file.name
                        private_key_file.write(bytes(private_key, "utf-8"))
                        private_key_file.close()
                        ssl_args["ssl_keyfile"] = private_key_path

                        demisto.debug("Starting HTTPS Server")
                    else:
                        demisto.debug("Starting HTTP Server")

                    integration_logger = IntegrationLogger()
                    integration_logger.buffering = False
                    log_config = dict(uvicorn.config.LOGGING_CONFIG)
                    log_config["handlers"]["default"]["stream"] = integration_logger
                    log_config["handlers"]["access"]["stream"] = integration_logger
                    log_config["formatters"]["access"] = {
                        "()": PublishListAccessFormatter,
                        "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s "%(user_agent)s"',
                    }
                    uvicorn.run(app, host="0.0.0.0", port=port, log_config=log_config, **ssl_args)  # type: ignore[arg-type]
                except Exception as e:
                    demisto.error(f"An error occurred in the long running loop: {str(e)} - {format_exc()}")
                    demisto.updateModuleHealth(f"An error occurred: {str(e)}")
                finally:
                    if certificate_path:
                        os.unlink(certificate_path)
                    if private_key_path:
                        os.unlink(private_key_path)
                    time.sleep(5)
    except Exception as e:
        demisto.error(format_exc())
        return_error(f"Failed to execute {demisto.command()} command. Error: {e}")


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