appNovi

Search across meshed network, security, and business data in appNovi to make efficient informed security decisions for risk management and incident response. Gain immediate intelligence on assets, visualize risk and threats across your network, and undertake interactive investigations across the network to reduce MTTR for incident response.

Analytics & SIEM · AppNovi

Details

IDappNovi
ProviderFenix24
CategoryAnalytics & SIEM
From Version6.5.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Search across meshed network, security, and business data in appNovi to make efficient informed security decisions for risk management and incident response. Gain immediate intelligence on assets, visualize risk and threats across your network, and undertake interactive investigations across the network to reduce MTTR for incident response.

This integration was integrated and tested with appNovi v2.0

Configure appNovi in Cortex

Parameter Description Required
Full URL of appNovi instance API.   True
API Token appNovi token URL for authentication True
Trust any certificate (not secure)   False
Use system proxy settings   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.

search-appnovi-components


Search for Components by name or value

Base Command

search-appnovi-components

Input

Argument Name Description Required
search_term The string to use to search for Components. Required
max_results Number of results. Default is 25. Optional

Context Output

Path Type Description
appnovi.components.name String Name of thing
appnovi.components.coll String Collection containing thing
appnovi.components.u._id String appNovi Database ID.
appnovi.components.u._key String appNovi Database Key.
appnovi.components.u.identity.company String Company ID
appnovi.components.u.identity.type String Thing type
appnovi.components.u.identity.value String Thing name
appnovi.components.u.identity.datacenter String Datacenter ID
appnovi.components.u.identity.domain String Domain ID
appnovi.components.u.lastSeen Date Time thing was last seen.
appnovi.components.u.source Date This is information about the source of the entity.
appnovi.components.u.userProperties String These are the custom properties of the entity.
appnovi.components.connections Number Number of connections.
appnovi.time Number Query time (for diagnostics)

search-appnovi-connected


Search for Components connected to supplied identity

Base Command

search-appnovi-connected

Input

Argument Name Description Required
identity appNovi identifier key-value or use key “_id” when searching via appNovi ID. Required
category Optional category of components to return. Possible values are: BaseComponent, Identity, IPAddress, Interface, Server, Storage, Hypervisor, CloudInfra, CloudService, NetworkInfra, Cluster, Container, MessageQueue, Vulnerability, CVE, Employee, Client, Software, Policy, Databases. Optional
type Optional type of components to return. Possible values are: genericcomponent, genericmetadata, rdns, user, machine, iamrole, iamuser, ip, ipv4, ipv6, mac, eni, interface, vmwarevm, ec2, azurevm, vm, server, physical, s3, datastore, bucket, storage, esx, vcenter, hypervisor, vpc, subscription, account, resourcegroup, region, availabilityzone, autoscalinggroup, elasticloadbalancer, org, folder, project, vnet, rds, ecr, lambda, redshift, dynamodbtable, router, firewall, switch, loadbalancer, proxy, middleware, kubernetes, aks, eks, swarm, mesos, nomad, docker, containerd, messagetopic, messageserver, vulnerability, cve, employee, contractor, desktop, laptop, pc, vdi, mobile, client, software, ami, securitygroup, iampolicy, column, database, table, view. Optional
max_results Number of results. Default is 25. Optional

Context Output

Path Type Description
appnovi._key String appNovi database key
appnovi._id String appNovi database ID.
appnovi.category String appNovi Category (e.g. Interface, Server)
appnovi.firstSeen Date When was this Thing first seen?
appnovi.identity.company String Company ID
appnovi.identity.datacenter String Datacenter ID
appnovi.identity.domain String Domain ID
appnovi.identity.type String Thing type
appnovi.identity.value String Identity value
appnovi.lastSeen Date Last time thing was seen
appnovi.name String Name of entity.
appnovi.source Unknown This is information about the source of the entity.
appnovi.userProperties String These are the custom properties of the entity.
appnovi.applications String List of applications

search-appnovi-cve


Search for servers with matching CVE

Base Command

search-appnovi-cve

Input

Argument Name Description Required
cve CVE e.g. “CVE-2017-0143”. Required

Context Output

Path Type Description
appnovi._key String appNovi database key
appnovi._id String appNovi database ID.
appnovi.category String appNovi Category (e.g. Interface, Server)
appnovi.firstSeen Date When was this Thing first seen?
appnovi.identity.company String Company ID
appnovi.identity.datacenter String Datacenter ID
appnovi.identity.domain String Domain ID
appnovi.identity.type String Thing type
appnovi.identity.value String Identity value
appnovi.lastSeen Date Last time thing was seen
appnovi.name String Name of entity.
appnovi.source Unknown This is information about the source of the entity.
appnovi.userProperties String These are the custom properties of the entity.
appnovi.applications String List of applications

search-appnovi-component-property


Search for Components by property and value

Base Command

search-appnovi-component-property

Input

Argument Name Description Required
property Name of property. Required
value Value of property. Required
max_results Number of results. Default is 25. Optional

Context Output

Path Type Description
appnovi.components.name String Name of thing
appnovi.components.coll String Collection containing thing
appnovi.components.u._id String appNovi Database ID.
appnovi.components.u._key String appNovi Database Key.
appnovi.components.u.identity.company String Company ID
appnovi.components.u.identity.type String Thing type
appnovi.components.u.identity.value String Thing name
appnovi.components.u.identity.datacenter String Datacenter ID
appnovi.components.u.identity.domain String Domain ID
appnovi.components.u.lastSeen Date Time thing was last seen.
appnovi.components.u.source Date This is information about the source of the entity.
appnovi.components.u.userProperties String These are the custom properties of the entity.
appnovi.components.connections Number Number of connections.
appnovi.time Number Query time (for diagnostics)

search-appnovi-server-by-ip


Search for servers using IP address

Base Command

search-appnovi-server-by-ip

Input

Argument Name Description Required
ip Server IP to search. Required

Context Output

Path Type Description
appnovi._key String appNovi database key
appnovi._id String appNovi database ID.
appnovi.category String appNovi Category (e.g. Interface, Server)
appnovi.firstSeen Date When was this Thing first seen?
appnovi.identity.company String Company ID
appnovi.identity.datacenter String Datacenter ID
appnovi.identity.domain String Domain ID
appnovi.identity.type String Thing type
appnovi.identity.value String Identity value
appnovi.lastSeen Date Last time thing was seen
appnovi.name String Name of entity.
appnovi.source Unknown This is information about the source of the entity.
appnovi.userProperties String These are the custom properties of the entity.
appnovi.applications String List of applications

Configuration parameters

  • appnovi_url — Full URL of appNovi instance. (required)
  • appnovi_token — API Token (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (5)

  • search-appnovi-component-property

    Search for Components by property and value.

  • search-appnovi-components

    Search for Components by name or value.

  • search-appnovi-connected

    Search for Components connected to supplied identity.

  • search-appnovi-cve

    Search for servers with matching CVE.

  • search-appnovi-server-by-ip

    Search for servers using IP address.

import demistomock as demisto
from CommonServerPython import *
from CommonServerUserPython import *
import json
from typing import Any

import urllib3

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

API_PREFIX = "/api/v1"

""" CLIENT CLASS """


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

    Interaction with the appNovi API
    """

    def get_search_results(self, search_term: str, max_results: int = 25) -> dict[str, Any]:
        """Gets the IP reputation using the '/ip' API endpoint

        :type search_term: ``str``
        :param search_term: Search for anything in appNovi

        :return: dict containing the results as returned from the API
        :rtype: ``Dict[str, Any]``
        """

        return self._http_request(
            method="GET",
            url_suffix="/components/search",
            params={
                "string": search_term,
                "include_properties": True,
                "max_results": max_results,
            },
        )

    def get_connected_results(
        self,
        search_identity: str | dict,
        connect_type: Optional[list] = None,
        connect_category: Optional[list] = None,
        max_results: int = 25,
    ) -> list[dict[str, Any]]:
        # Can't use json= since it's already in use by xsoar
        return self._http_request(
            method="POST",
            url_suffix="/components/connected",
            data=json.dumps([search_identity]),
            params={
                "max_results": max_results,
                "type": connect_type,
                "category": connect_category,
            },
        )

    def get_types(self):
        return self._http_request(
            method="GET",
            url_suffix="/components/types",
        )

    def get_prop_search_results(self, prop: str, value: str, max_results: int = 25) -> dict:
        return self._http_request(
            method="GET",
            url_suffix="/components/propsearch",
            params={
                "prop": prop,
                "value": value,
                "include_properties": True,
                "max_results": max_results,
            },
        )


""" HELPER FUNCTIONS """


def dict_by_path(full_dict: dict, dict_path: str) -> Any:
    """Get values from dictionary by path

    just some code
    """
    paths = dict_path.split(".")
    return_value = full_dict
    for path in paths:
        try:
            return_value = return_value[path]
        except (KeyError, TypeError):
            return None
    return return_value


def process_sources(source_dict: dict) -> str:
    """Parse source dict into something readable"""
    source = source_dict.keys()
    return ",".join([s for s in source if len(s)])


""" COMMAND FUNCTIONS """


def test_module(client: Client) -> str:
    """Test Integration

    :type client: ``Client``

    :return:
        A ``str`` representing if authentication was successful

    :rtype: ``str``
    """

    # Call the Client function and get the raw response
    try:
        client.get_types()
    except DemistoException as e:
        if "Forbidden" in str(e):
            return "Authorization Error: make sure API Key is correctly set"
        else:
            raise e
    return "ok"


def search_appnovi_command(client: Client, args: dict[str, Any]) -> CommandResults:
    search_term = args.get("search_term", None)
    if not search_term:
        raise ValueError("Search term not specified")

    results = client.get_search_results(search_term, args.get("max_results", 25))

    table_layout = {
        "name": "name",
        "appnoviid": "u._id",
        "type": "u.identity.type",
        "value": "u.identity.value",
        "lastSeen": "u.lastSeen",
        "connections": "connections",
    }

    readable_output = "### Search Results\n" + " | ".join(table_layout.keys()) + " | sources" + "\n"
    readable_output += "|".join(["-----" for th in table_layout]) + "|----" + "\n"

    for result in results["components"]:
        readable_output += (
            " | ".join([str(dict_by_path(result, f)) for f in table_layout.values()])
            + " | "
            + process_sources(result["u"].get("source", {}))
            + "\n"
        )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="appnovi.search",
        outputs_key_field=table_layout["appnoviid"],
        outputs=results,
    )


def search_appnovi_prop_command(client: Client, args: dict[str, Any]) -> CommandResults:
    search_prop = args.get("property", None)
    search_value = args.get("value", None)

    if not search_prop or not search_value:
        raise ValueError("Search terms not specified")

    results = client.get_prop_search_results(search_prop, search_value, args.get("max_results", 25))

    table_layout = {
        "name": "name",
        "appnoviid": "u._id",
        "type": "u.identity.type",
        "value": "u.identity.value",
        "lastSeen": "u.lastSeen",
        "connections": "connections",
    }

    readable_output = "### Search Results\n" + " | ".join(table_layout.keys()) + " | sources" + "\n"
    readable_output += "|".join(["-----" for th in table_layout]) + "|----" + "\n"

    for result in results["components"]:
        readable_output += (
            " | ".join([str(dict_by_path(result, f)) for f in table_layout.values()])
            + " | "
            + process_sources(result["u"].get("source", {}))
            + "\n"
        )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="appnovi.searchProp",
        outputs_key_field=table_layout["appnoviid"],
        outputs=results,
    )


def search_appnovi_connected_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Search for components connected to other components.
    Can be limited in the types of things returned"""

    identity = args.get("identity", None)
    if not identity:
        raise ValueError("Identity not specified")

    if appnovi_id := identity.get("_id", None):
        identity = appnovi_id

    # Check for arguments
    cats = argToList(args.get("category", "")) or None
    types = argToList(args.get("type", "")) or None

    # Process identity
    results = client.get_connected_results(identity, types, cats)

    table_layout = {
        "name": "name",
        "appnoviid": "_id",
        "category": "category",
        "type": "identity.type",
        "value": "identity.value",
    }

    readable_output = "### Search Results\n" + " | ".join(table_layout.keys()) + "\n"
    readable_output += "|".join(["-----" for th in table_layout]) + "\n"

    for result in results:
        readable_output += " | ".join([str(dict_by_path(result, f)) for f in table_layout.values()]) + "\n"

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="appnovi.connected",
        outputs_key_field=table_layout["appnoviid"],
        outputs=results,
    )


def search_appnovi_cve_servers_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Find Servers with CVE
    This is a convenience command using the connected search"""

    cve = args.get("cve", None)
    if not cve:
        raise ValueError("CVE not specified")

    results = client.get_connected_results({"type": "cve", "value": cve.upper()}, None, ["Server"])

    table_layout = {
        "name": "name",
        "appnoviid": "_id",
        "category": "category",
        "type": "identity.type",
        "value": "identity.value",
    }

    readable_output = "### Search Results\n" + " | ".join(table_layout.keys()) + "\n"
    readable_output += "|".join(["-----" for th in table_layout]) + "\n"

    for result in results:
        readable_output += " | ".join([str(dict_by_path(result, f)) for f in table_layout.values()]) + "\n"

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="appnovi.cveServers",
        outputs_key_field=table_layout["appnoviid"],
        outputs=results,
    )


def find_server_by_ip_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Use the connected function to return servers owning a given IP
    Good example of how you an chain requests to walk the graph via command or playbook
    """
    ip = args.get("ip", None)
    if not ip:
        raise ValueError("IP not specified")

    # Keep track of things
    servers = {}
    interfaces: list[str] = []

    # Let's get any servers or interfaces connected to the IP
    first_walk = client.get_connected_results({"type": "ip", "value": ip}, None, ["Server", "Interface"])

    # Examine first walk
    for thing in first_walk:
        category = thing.get("category", None)
        # Collect interfaces for next walk
        if category == "Interface":
            # Make mypy happy by checking thing.get("_id") for str type.
            _id = thing.get("_id")
            if isinstance(_id, str):
                interfaces.append(_id)

        # Servers are usually not directly connected to IP, but in case...
        if category == "Server":
            servers[thing.get("_id")] = thing

    # Walk each interface. Serching by _id is /very/ fast
    for interface in interfaces:
        possible_server = client.get_connected_results(interface, None, ["Server"])
        for server in possible_server:
            servers[server.get("_id")] = server

    # Walk finished, output some results
    readable_output = f"### Servers with IP {ip}\n"
    if len(servers.keys()):
        table_layout = {
            "name": "name",
            "appnoviid": "_id",
            "type": "identity.type",
            "value": "identity.value",
        }
        readable_output = "### Search Results\n" + " | ".join(table_layout.keys()) + "\n"
        readable_output += "|".join(["-----" for th in table_layout]) + "\n"

        for result in servers.values():
            readable_output += " | ".join([str(dict_by_path(result, f)) for f in table_layout.values()]) + "\n"

    else:
        readable_output += "No Results \n"

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="appnovi.server",
        outputs_key_field="",
        outputs=[v for k, v in servers.items()],
    )


""" MAIN FUNCTION """


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

    :return:
    :rtype:
    """

    params = demisto.params()
    command = demisto.command()

    api_key = params.get("appnovi_token")
    base_url = urljoin(params["appnovi_url"], API_PREFIX)

    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    demisto.debug(f"Command being called is {command}")
    try:
        headers = {"Authorization": f"Bearer {api_key}"}
        client = Client(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy)

        command = demisto.command()

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

        commands = {
            "search-appnovi-components": search_appnovi_command,
            "search-appnovi-component-property": search_appnovi_prop_command,
            "search-appnovi-connected": search_appnovi_connected_command,
            "search-appnovi-cve": search_appnovi_cve_servers_command,
            "search-appnovi-server-by-ip": find_server_by_ip_command,
        }

        fn = commands.get(command, None)
        if fn:
            return_results(fn(client, demisto.args()))

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


""" ENTRY POINT """

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