Anomali Enterprise

Use Anomali Match to search indicators and enrich domains.

Data Enrichment & Threat Intelligence · Anomali Enterprise

Details

IDAnomali Enterprise
ProviderAnomali
CategoryData Enrichment & Threat Intelligence
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Use Anomali Match to search indicators and enrich domains.

Configure Anomali Match in Cortex

Parameter Description Required
url Server URL (e.g., https://www.test.com) True
credentials Username True
insecure Trust any certificate (not secure) False
proxy 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.

anomali-enterprise-retro-forensic-search


Initiates a forensic search of the indicators.

Base Command

anomali-enterprise-retro-forensic-search

Input

Argument Name Description Required
from The time the indicators first appeared, in the format: <number> <time unit>, e.g., 1 hour, 30 minutes. Default is 1 day ago. Optional
to The time the indicators last appeared, in the format: <number> <time unit>, e.g., 1 hour, 30 minutes. Default is now. Optional
indicators A comma-separated list of indicators to search. Required

Context Output

Path Type Description
AnomaliEnterprise.ForensicSearch.job_id String The job ID of the search.
AnomaliEnterprise.ForensicSearch.status String The status of the search.

Command Example

!anomali-enterprise-retro-forensic-search indicators=1.1.1.1 from="1 month"

Context Example

{
    "AnomaliEnterprise": {
        "ForensicSearch": {
            "job_id": "job1271604409989806",
            "status": "in progress"
        }
    }
}

Human Readable Output

Forensic search started

job_id status
job1271604409989806 in progress

anomali-enterprise-retro-forensic-search-results


Retrieves the forensic search results.

Base Command

anomali-enterprise-retro-forensic-search-results

Input

Argument Name Description Required
job_id The forensic search job ID. Required
limit Limit the stream results to return. Default is 20. Optional
verbose Whether to print the stream results to the War Room. Default is “true”. Optional

Context Output

Path Type Description
AnomaliEnterprise.ForensicSearch.job_id String The job ID of the search.
AnomaliEnterprise.ForensicSearch.status String The status of the search.
AnomaliEnterprise.ForensicSearch.scannedEvents Number The number of scanned events.
AnomaliEnterprise.ForensicSearch.processedFiles Number The number of processed files.
AnomaliEnterprise.ForensicSearch.result_file_name String The matched file name.
AnomaliEnterprise.ForensicSearch.totalMatches Number The number of total matches.
AnomaliEnterprise.ForensicSearch.complete Bool Whether the search was complete.
AnomaliEnterprise.ForensicSearch.category String The search category.
AnomaliEnterprise.ForensicSearch.streamResults Unknown The stream results for the search.

Command Example

!anomali-enterprise-retro-forensic-search-results job_id=job1251604409794526

Context Example

{
    "AnomaliEnterprise": {
        "ForensicSearch": {
            "category": "forensic_api_result",
            "complete": true,
            "job_id": "job1251604409794526",
            "processedFiles": 1,
            "result_file_name": "org0_20201103_job1251604409794526_result.tar.gz",
            "scannedEvents": 361295,
            "status": "completed",
            "streamResults": [
                {
                    "age": "",
                    "confidence": "",
                    "count": "1",
                    "event.dest": "1.1.1.1",
                    "event.src": "1.1.1.1",
                    "event_time": "2020-10-14T09:10:00.000+0000",
                    "indicator": "",
                    "itype": "",
                    "severity": ""
                }
            ],
            "totalFiles": 1,
            "totalMatches": 1
        }
    }
}

Human Readable Output

Forensic search metadata

status job_id category totalFiles scannedEvents
completed job1251604409794526 forensic_api_result 1 361295

Forensic search results

count event.dest event.src event_time
1 1.1.1.1 1.1.1.1 2020-10-14T09:10:00.000+0000

anomali-enterprise-dga-domain-status


The search domains Domain Generation Algorithm (DGA).

Base Command

anomali-enterprise-dga-domain-status

Input

Argument Name Description Required
domains A comma-separated list of domains to search. Required

Context Output

Path Type Description
AnomaliEnterprise.DGA.domain String The domain that was checked.
AnomaliEnterprise.DGA.malware_family String The malware family associated with the domain.
AnomaliEnterprise.DGA.domain Number The probability of the domain being malicious.

Command Example

!anomali-enterprise-dga-domain-status domains=amazon.com

Context Example

{
    "AnomaliEnterprise": {
        "DGA": {
            "domain": "amazon.com",
            "malware_family": "",
            "probability": 0
        }
    }
}

Human Readable Output

Domains DGA

domain probability
amazon.com 0

domain


The search domains Domain Generation Algorithm (DGA). Includes DBotScore and domain information.
There is no distinction between benign to unknown domains in Anomali Enterprise.
The Domain reputation is calculated per the product documentation.
if malware family exists and prob > 0.6 the reputation is Malicious,
if malware family exists and prob < 0.6 the reputation is Suspicious,
else, the reputation is Unknown.

Base Command

domain

Input

Argument Name Description Required
domain A comma-separated list of domains to search. Optional

Context Output

Path Type Description
AnomaliEnterprise.DGA.domain String The domain that was checked.
AnomaliEnterprise.DGA.malware_family String The malware family associated with the domain.
AnomaliEnterprise.DGA.domain Number The probability of the domain being malicious.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.
DBotScore.Score Number The actual score.
Domain.Name String The domain name. For example, “google.com”.
Domain.Malicious.Vendor String The vendor that reported that the domain is malicious.
Domain.Malicious.Description String A description of the malicious domain.

Command Example

!domain domain=google.com

Context Example

{
    "AnomaliEnterprise": {
        "DGA": {
            "domain": "google.com",
            "malware_family": "",
            "probability": 0
        }
    },
    "DBotScore": {
        "Indicator": "google.com",
        "Score": 0,
        "Type": "domain",
        "Vendor": "Anomali Enterprise"
    },
    "Domain": {
        "Name": "google.com"
    }
}

Human Readable Output

Domains DGA

domain probability
google.com 0

Configuration parameters

  • url — Server URL (e.g., https://www.test.com) (required)
  • credentials — Username (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • integrationReliability — Source Reliability
  • feedExpirationPolicy
  • feedExpirationInterval

Commands (4)

  • anomali-enterprise-dga-domain-status

    The search domains Domain Generation Algorithm (DGA).

  • anomali-enterprise-retro-forensic-search

    Initiates a forensic search of the indicators.

  • anomali-enterprise-retro-forensic-search-results

    Retrieves the forensic search results.

  • domain

    The search domains Domain Generation Algorithm (DGA). Includes DBotScore and domain information.

import urllib3
from CommonServerPython import *

# Disable insecure warnings
urllib3.disable_warnings()

VENDOR_NAME = "Anomali Enterprise"

""" CLIENT CLASS """


class Client(BaseClient):
    """
    Client to use in the Anomali Enterprise integration. Overrides BaseClient
    """

    def __init__(self, server_url: str, username: str, password: str, verify: bool, proxy: bool):
        headers = {"Content-Type": "application/json", "ae-authorization": f"{username}:{password}"}
        super().__init__(base_url=server_url, verify=verify, proxy=proxy, headers=headers)
        self._username = username
        self._password = password

    def start_search_job_request(self, from_: str, to_: str, indicators: list[str]) -> dict:
        """Initiate a search job.
        Args:
            from_: from which time to initiate the search
            to_: to which time to initiate the search
            indicators: indicators to search
        Returns:
            Response from API.
        """
        data = {"username": self._username, "password": self._password, "from": from_, "to": to_, "indicators": indicators}
        return self._http_request(method="POST", url_suffix="/api/v1/mars/forensic", headers=self._headers, json_data=data)

    def get_search_job_result_request(self, job_id: str) -> dict:
        """Retrieve a search job results.
        Args:
            job_id: the search job uuid
        Returns:
            Response from API.
        """
        params = {"jobid": job_id}
        return self._http_request(method="GET", url_suffix="/api/v1/mars/forensic", headers=self._headers, params=params)

    def domain_request(self, domain: list[str]) -> dict:
        """Retrieve information regarding a domain.
        Args:
            domain: the domain name to search
        Returns:
            Response from API.
        """
        data = {"username": self._username, "password": self._password, "domains": domain}
        return self._http_request(method="POST", url_suffix="/api/v1/mars/dga_score", headers=self._headers, json_data=data)


""" COMMAND FUNCTIONS """


def module(client: Client) -> str:
    """
    Performs basic get request
    """
    response = client.domain_request(argToList("google.com"))
    if response.get("result") != "success":
        raise Exception(
            "To Use Anomali Enterprise, make sure you are using the current username and password "
            "and have the needed permissions."
        )
    return "ok"


def start_search_job(client: Client, args: dict) -> CommandResults:
    """Start a search job for IOCs.

    Args:
        client: Client object with request.
        args: Usually demisto.args()

    Returns:
        CommandResults.
    """
    from_ = str(args.get("from", "1 day"))
    to_ = str(args.get("to", ""))
    indicators = argToList(args.get("indicators"))

    timestamp_format = "%Y-%m-%dT%H:%M:%S.%f"
    from_iso = parse_date_range(from_, date_format=timestamp_format)[0]
    if to_:
        to_iso = parse_date_range(to_, date_format=timestamp_format)[0]
    else:
        to_iso = datetime.now().strftime(timestamp_format)

    response = client.start_search_job_request(from_iso, to_iso, indicators)

    start_search_outputs = {"status": "in progress", "job_id": response.get("jobid", "")}

    return CommandResults(
        outputs_prefix="AnomaliEnterprise.ForensicSearch",
        outputs_key_field="job_id",
        outputs=start_search_outputs,
        readable_output=tableToMarkdown(name="Forensic search started:", t=start_search_outputs, removeNull=True),
        raw_response=response,
    )


def get_search_job_result(client: Client, args: dict) -> list[CommandResults]:
    """Get the search job result.

    Args:
        client: Client object with request.
        args: Usually demisto.args()

    Returns:
        CommandResults.
    """
    job_ids = argToList(args.get("job_id"))
    limit = int(args.get("limit", "20"))
    verbose = args.get("verbose", "true") == "true"

    command_results: list = []
    for job_id in job_ids:
        response = client.get_search_job_result_request(job_id)
        if "error" in response:
            raise Exception(f"{response.get('error')!s}. Job ID might have expired.")

        outputs = response
        outputs.update({"job_id": job_id})
        if not response.get("complete"):
            human_readable = f"job ID: {job_id} is still in progress."
            outputs.update({"status": "in progress"})
        else:
            if response.get("totalMatches"):
                headers = ["status", "job_id", "category", "totalFiles", "scannedEvents"]
                human_readable = tableToMarkdown(name="Forensic search metadata:", t=response, headers=headers, removeNull=True)
                if verbose:
                    human_readable += tableToMarkdown(
                        name="Forensic search results:", t=response.get("streamResults", [])[:limit], removeNull=True
                    )
                if "streamResults" in outputs:
                    outputs["streamResults"] = outputs.get("streamResults", [])[:limit]  # limit the outputs to the context
            else:
                human_readable = f"No matches found for the given job ID: {job_id}."
                response.update({"status": "completed"})

        command_result = CommandResults(
            outputs_prefix="AnomaliEnterprise.ForensicSearch",
            outputs_key_field="job_id",
            outputs=response,
            readable_output=human_readable,
            raw_response=response,
        )
        command_results.append(command_result)
    return command_results


def dga_domain_status(client: Client, args: dict) -> CommandResults:
    """Search domain DGA status.

    Args:
        client: Client object with request.
        args: Usually demisto.args()

    Returns:
        CommandResults.
    """
    domains = argToList(str(args.get("domains")))

    response = client.domain_request(domains)

    domains_data = response.get("data", {})
    outputs = []
    for domain in domains:
        output = {
            "domain": domain,
            "malware_family": domains_data.get(domain, {}).get("malware_family"),
            "probability": domains_data.get(domain, {}).get("probability"),
        }
        outputs.append(output)
    return CommandResults(
        outputs_prefix="AnomaliEnterprise.DGA",
        outputs_key_field="domain",
        outputs=outputs,
        readable_output=tableToMarkdown(name="Domains DGA:", t=outputs, removeNull=True),
        raw_response=response,
    )


def domain_command(client: Client, args: dict) -> list[CommandResults]:
    """Search domain DGA status.

    Args:
        client: Client object with request.
        args: Usually demisto.args()

    Returns:
        CommandResults and DBotScore.
    """
    domain_list = argToList(args.get("domain"))

    response = client.domain_request(domain_list)
    domains_data = response.get("data", {})
    command_results_list = []
    for domain in domain_list:
        output = {
            "domain": domain,
            "malware_family": domains_data.get(domain, {}).get("malware_family"),
            "probability": domains_data.get(domain, {}).get("probability"),
        }
        score = calculate_dbot_score(domains_data.get(domain, {}))

        dbot_score = Common.DBotScore(
            indicator=domain,
            indicator_type=DBotScoreType.DOMAIN,
            integration_name=VENDOR_NAME,
            score=score,
            malicious_description=str(output.get("malware_family", "")),
            reliability=demisto.params().get("integrationReliability"),
        )

        domain = Common.Domain(
            domain=domain,
            dbot_score=dbot_score,
            tags="DGA" if score in [Common.DBotScore.SUSPICIOUS, Common.DBotScore.BAD] else None,
        )

        command_results = CommandResults(
            outputs_prefix="AnomaliEnterprise.DGA",
            outputs_key_field="domain",
            outputs=output,
            readable_output=tableToMarkdown(name="Domains DGA:", t=output, removeNull=True),
            indicator=domain,
            raw_response=response,
        )
        command_results_list.append(command_results)

    return command_results_list


def calculate_dbot_score(domain_data: dict) -> int:
    """There is no distinction between benign to unknown domains in Anomali Enterprise
    malware family exists and prob > 0.6 -> 3
    malware family exists and prob < 0.6 -> 2
    else -> 0

    Args:
        domain_data: the domain data

    Returns:
        DBot Score.
    """
    score = Common.DBotScore.NONE
    if domain_data.get("malware_family", {}):
        if float(domain_data.get("probability", 0)) > 0.6:
            score = Common.DBotScore.BAD
        else:
            score = Common.DBotScore.SUSPICIOUS
    return score


""" MAIN FUNCTION """


def main() -> None:
    """
    Parse and validates integration params, runs integration commands.
    """
    params = demisto.params()
    server_url = params.get("url")
    username = params.get("credentials", {}).get("identifier")
    password = params.get("credentials", {}).get("password")
    verify = not params.get("insecure", False)
    proxy = params.get("proxy") is True

    command = demisto.command()
    LOG(f"Command being called in {VENDOR_NAME} is: {command}")

    try:
        client = Client(server_url=server_url, username=username, password=password, verify=verify, proxy=proxy)
        commands = {
            "anomali-enterprise-retro-forensic-search": start_search_job,
            "anomali-enterprise-retro-forensic-search-results": get_search_job_result,
            "anomali-enterprise-dga-domain-status": dga_domain_status,
            "domain": domain_command,
        }
        if command == "test-module":
            return_results(module(client))
        elif command in commands:
            return_results(commands[command](client, demisto.args()))
        else:
            raise NotImplementedError(f'Command "{command}" is not implemented.')

    except Exception as err:
        return_error(f"Failed to execute {command} command. Error: {err!s} \n tracback: {traceback.format_exc()}")


""" ENTRY POINT """

if __name__ in ["__main__", "builtin", "builtins"]:
    main()