mirror of https://github.com/MISP/misp-modules
				
				
				
			
		
			
				
	
	
		
			622 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			622 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
| #!/usr/bin/env python3
 | |
| """
 | |
| Expansion module integrating with VMware NSX Defender.
 | |
| """
 | |
| import argparse
 | |
| import base64
 | |
| import configparser
 | |
| import datetime
 | |
| import hashlib
 | |
| import io
 | |
| import ipaddress
 | |
| import json
 | |
| import logging
 | |
| import pymisp
 | |
| import sys
 | |
| import vt
 | |
| import zipfile
 | |
| from urllib import parse
 | |
| from typing import Any, Dict, List, Optional, Tuple, Union
 | |
| 
 | |
| import tau_clients
 | |
| from tau_clients import exceptions
 | |
| from tau_clients import nsx_defender
 | |
| 
 | |
| 
 | |
| logger = logging.getLogger("vmware_nsx")
 | |
| logger.setLevel(logging.DEBUG)
 | |
| 
 | |
| misperrors = {
 | |
|     "error": "Error",
 | |
| }
 | |
| 
 | |
| mispattributes = {
 | |
|     "input": [
 | |
|         "attachment",
 | |
|         "malware-sample",
 | |
|         "url",
 | |
|         "md5",
 | |
|         "sha1",
 | |
|         "sha256",
 | |
|     ],
 | |
|     "format": "misp_standard",
 | |
| }
 | |
| 
 | |
| moduleinfo = {
 | |
|     "version": "0.2",
 | |
|     "author": "Jason Zhang, Stefano Ortolani",
 | |
|     "description": "Enrich a file or URL with VMware NSX Defender",
 | |
|     "module-type": ["expansion", "hover"],
 | |
| }
 | |
| 
 | |
| moduleconfig = [
 | |
|     "analysis_url",             # optional, defaults to hard-coded values
 | |
|     "analysis_verify_ssl",      # optional, defaults to True
 | |
|     "analysis_key",             # required
 | |
|     "analysis_api_token",       # required
 | |
|     "vt_key",                   # optional
 | |
|     "misp_url",                 # optional
 | |
|     "misp_verify_ssl",          # optional, defaults to True
 | |
|     "misp_key",                 # optional
 | |
| ]
 | |
| 
 | |
| DEFAULT_ZIP_PASSWORD = b"infected"
 | |
| 
 | |
| DEFAULT_ENDPOINT = tau_clients.NSX_DEFENDER_DC_WESTUS
 | |
| 
 | |
| WORKFLOW_COMPLETE_TAG = "workflow:state='complete'"
 | |
| 
 | |
| WORKFLOW_INCOMPLETE_TAG = "workflow:state='incomplete'"
 | |
| 
 | |
| VT_DOWNLOAD_TAG = "vt:download"
 | |
| 
 | |
| GALAXY_ATTACK_PATTERNS_UUID = "c4e851fa-775f-11e7-8163-b774922098cd"
 | |
| 
 | |
| 
 | |
| class ResultParser:
 | |
|     """This is a parser to extract *basic* information from a result dictionary."""
 | |
| 
 | |
|     def __init__(self, techniques_galaxy: Optional[Dict[str, str]] = None):
 | |
|         """Constructor."""
 | |
|         self.techniques_galaxy = techniques_galaxy or {}
 | |
| 
 | |
|     def parse(self, analysis_link: str, result: Dict[str, Any]) -> pymisp.MISPEvent:
 | |
|         """
 | |
|         Parse the analysis result into a MISP event.
 | |
| 
 | |
|         :param str analysis_link: the analysis link
 | |
|         :param dict[str, any] result: the JSON returned by the analysis client.
 | |
|         :rtype: pymisp.MISPEvent
 | |
|         :return: a MISP event
 | |
|         """
 | |
|         misp_event = pymisp.MISPEvent()
 | |
| 
 | |
|         # Add analysis subject info
 | |
|         if "url" in result["analysis_subject"]:
 | |
|             o = pymisp.MISPObject("url")
 | |
|             o.add_attribute("url", result["analysis_subject"]["url"])
 | |
|         else:
 | |
|             o = pymisp.MISPObject("file")
 | |
|             o.add_attribute("md5", type="md5", value=result["analysis_subject"]["md5"])
 | |
|             o.add_attribute("sha1", type="sha1", value=result["analysis_subject"]["sha1"])
 | |
|             o.add_attribute("sha256", type="sha256", value=result["analysis_subject"]["sha256"])
 | |
|             o.add_attribute(
 | |
|                 "mimetype",
 | |
|                 category="Payload delivery",
 | |
|                 type="mime-type",
 | |
|                 value=result["analysis_subject"]["mime_type"]
 | |
|             )
 | |
|         misp_event.add_object(o)
 | |
| 
 | |
|         # Add HTTP requests from url analyses
 | |
|         network_dict = result.get("report", {}).get("analysis", {}).get("network", {})
 | |
|         for request in network_dict.get("requests", []):
 | |
|             if not request["url"] and not request["ip"]:
 | |
|                 continue
 | |
|             o = pymisp.MISPObject(name="http-request")
 | |
|             o.add_attribute("method", "GET")
 | |
|             if request["url"]:
 | |
|                 parsed_uri = parse.urlparse(request["url"])
 | |
|                 o.add_attribute("host", parsed_uri.netloc)
 | |
|                 o.add_attribute("uri", request["url"])
 | |
|             if request["ip"]:
 | |
|                 o.add_attribute("ip-dst", request["ip"])
 | |
|             misp_event.add_object(o)
 | |
| 
 | |
|         # Add network behaviors from files
 | |
|         for subject in result.get("report", {}).get("analysis_subjects", []):
 | |
| 
 | |
|             # Add DNS requests
 | |
|             for dns_query in subject.get("dns_queries", []):
 | |
|                 hostname = dns_query.get("hostname")
 | |
|                 # Skip if it is an IP address
 | |
|                 try:
 | |
|                     if hostname == "wpad" or hostname == "localhost":
 | |
|                         continue
 | |
|                     # Invalid hostname, e.g., hostname: ZLKKJRPY or 2.2.0.10.in-addr.arpa.
 | |
|                     if "." not in hostname or hostname[-1] == ".":
 | |
|                         continue
 | |
|                     _ = ipaddress.ip_address(hostname)
 | |
|                     continue
 | |
|                 except ValueError:
 | |
|                     pass
 | |
| 
 | |
|                 o = pymisp.MISPObject(name="domain-ip")
 | |
|                 o.add_attribute("hostname", type="hostname", value=hostname)
 | |
|                 for ip in dns_query.get("results", []):
 | |
|                     o.add_attribute("ip", type="ip-dst", value=ip)
 | |
| 
 | |
|                 misp_event.add_object(o)
 | |
| 
 | |
|             # Add HTTP conversations (as network connection and as http request)
 | |
|             for http_conversation in subject.get("http_conversations", []):
 | |
|                 o = pymisp.MISPObject(name="network-connection")
 | |
|                 o.add_attribute("ip-src", http_conversation["src_ip"])
 | |
|                 o.add_attribute("ip-dst", http_conversation["dst_ip"])
 | |
|                 o.add_attribute("src-port", http_conversation["src_port"])
 | |
|                 o.add_attribute("dst-port", http_conversation["dst_port"])
 | |
|                 o.add_attribute("hostname-dst", http_conversation["dst_host"])
 | |
|                 o.add_attribute("layer3-protocol", "IP")
 | |
|                 o.add_attribute("layer4-protocol", "TCP")
 | |
|                 o.add_attribute("layer7-protocol", "HTTP")
 | |
|                 misp_event.add_object(o)
 | |
| 
 | |
|                 method, path, http_version = http_conversation["url"].split(" ")
 | |
|                 if http_conversation["dst_port"] == 80:
 | |
|                     uri = "http://{}{}".format(http_conversation["dst_host"], path)
 | |
|                 else:
 | |
|                     uri = "http://{}:{}{}".format(
 | |
|                         http_conversation["dst_host"],
 | |
|                         http_conversation["dst_port"],
 | |
|                         path
 | |
|                     )
 | |
|                 o = pymisp.MISPObject(name="http-request")
 | |
|                 o.add_attribute("host", http_conversation["dst_host"])
 | |
|                 o.add_attribute("method", method)
 | |
|                 o.add_attribute("uri", uri)
 | |
|                 o.add_attribute("ip-dst", http_conversation["dst_ip"])
 | |
|                 misp_event.add_object(o)
 | |
| 
 | |
|         # Add sandbox info like score and sandbox type
 | |
|         o = pymisp.MISPObject(name="sandbox-report")
 | |
|         sandbox_type = "saas" if tau_clients.is_task_hosted(analysis_link) else "on-premise"
 | |
|         o.add_attribute("score", result["score"])
 | |
|         o.add_attribute("sandbox-type", sandbox_type)
 | |
|         o.add_attribute("{}-sandbox".format(sandbox_type), "vmware-nsx-defender")
 | |
|         o.add_attribute("permalink", analysis_link)
 | |
|         misp_event.add_object(o)
 | |
| 
 | |
|         # Add behaviors
 | |
|         # Check if its not empty first, as at least one attribute has to be set for sb-signature object
 | |
|         if result.get("malicious_activity", []):
 | |
|             o = pymisp.MISPObject(name="sb-signature")
 | |
|             o.add_attribute("software", "VMware NSX Defender")
 | |
|             for activity in result.get("malicious_activity", []):
 | |
|                 a = pymisp.MISPAttribute()
 | |
|                 a.from_dict(type="text", value=activity)
 | |
|                 o.add_attribute("signature", **a)
 | |
|             misp_event.add_object(o)
 | |
| 
 | |
|         # Add mitre techniques
 | |
|         for techniques in result.get("activity_to_mitre_techniques", {}).values():
 | |
|             for technique in techniques:
 | |
|                 for misp_technique_id, misp_technique_name in self.techniques_galaxy.items():
 | |
|                     if technique["id"].casefold() in misp_technique_id.casefold():
 | |
|                         # If report details a sub-technique, trust the match
 | |
|                         # Otherwise trust it only if the MISP technique is not a sub-technique
 | |
|                         if "." in technique["id"] or "." not in misp_technique_id:
 | |
|                             misp_event.add_tag(misp_technique_name)
 | |
|                             break
 | |
|         return misp_event
 | |
| 
 | |
| 
 | |
| def _parse_submission_response(response: Dict[str, Any]) -> Tuple[str, List[str]]:
 | |
|     """
 | |
|     Parse the response from "submit_*" methods.
 | |
| 
 | |
|     :param dict[str, any] response: the client response
 | |
|     :rtype: tuple(str, list[str])
 | |
|     :return: the task_uuid and whether the analysis is available
 | |
|     :raises ValueError: in case of any error
 | |
|     """
 | |
|     task_uuid = response.get("task_uuid")
 | |
|     if not task_uuid:
 | |
|         raise ValueError("Submission failed, unable to process the data")
 | |
|     if response.get("score") is not None:
 | |
|         tags = [WORKFLOW_COMPLETE_TAG]
 | |
|     else:
 | |
|         tags = [WORKFLOW_INCOMPLETE_TAG]
 | |
|     return task_uuid, tags
 | |
| 
 | |
| 
 | |
| def _unzip(zipped_data: bytes, password: bytes = DEFAULT_ZIP_PASSWORD) -> bytes:
 | |
|     """
 | |
|     Unzip the data.
 | |
| 
 | |
|     :param bytes zipped_data: the zipped data
 | |
|     :param bytes password: the password
 | |
|     :rtype: bytes
 | |
|     :return: the unzipped data
 | |
|     :raises ValueError: in case of any error
 | |
|     """
 | |
|     try:
 | |
|         data_file_object = io.BytesIO(zipped_data)
 | |
|         with zipfile.ZipFile(data_file_object) as zip_file:
 | |
|             sample_hash_name = zip_file.namelist()[0]
 | |
|             return zip_file.read(sample_hash_name, password)
 | |
|     except (IOError, ValueError) as e:
 | |
|         raise ValueError(str(e))
 | |
| 
 | |
| 
 | |
| def _download_from_vt(client: vt.Client, file_hash: str) -> bytes:
 | |
|     """
 | |
|     Download file from VT.
 | |
| 
 | |
|     :param vt.Client client: the VT client
 | |
|     :param str file_hash: the file hash
 | |
|     :rtype: bytes
 | |
|     :return: the downloaded data
 | |
|     :raises ValueError: in case of any error
 | |
|     """
 | |
|     try:
 | |
|         buffer = io.BytesIO()
 | |
|         client.download_file(file_hash, buffer)
 | |
|         buffer.seek(0, 0)
 | |
|         return buffer.read()
 | |
|     except (IOError, vt.APIError) as e:
 | |
|         raise ValueError(str(e))
 | |
|     finally:
 | |
|         # vt.Client likes to free resources at shutdown, and it can be used as context to ease that
 | |
|         # Since the structure of the module does not play well with how MISP modules are organized
 | |
|         #   let's play nice and close connections pro-actively (opened by "download_file")
 | |
|         if client:
 | |
|             client.close()
 | |
| 
 | |
| 
 | |
| def _get_analysis_tags(
 | |
|     clients: Dict[str, nsx_defender.AnalysisClient],
 | |
|     task_uuid: str,
 | |
| ) -> List[str]:
 | |
|     """
 | |
|     Get the analysis tags of a task.
 | |
| 
 | |
|     :param dict[str, nsx_defender.AnalysisClient] clients: the analysis clients
 | |
|     :param str task_uuid: the task uuid
 | |
|     :rtype: list[str]
 | |
|     :return: the analysis tags
 | |
|     :raises exceptions.ApiError: in case of client errors
 | |
|     :raises exceptions.CommunicationError: in case of client communication errors
 | |
|     """
 | |
|     client = clients[DEFAULT_ENDPOINT]
 | |
|     response = client.get_analysis_tags(task_uuid)
 | |
|     tags = set([])
 | |
|     for tag in response.get("analysis_tags", []):
 | |
|         tag_header = None
 | |
|         tag_type = tag["data"]["type"]
 | |
|         if tag_type == "av_family":
 | |
|             tag_header = "av-fam"
 | |
|         elif tag_type == "av_class":
 | |
|             tag_header = "av-cls"
 | |
|         elif tag_type == "lastline_malware":
 | |
|             tag_header = "nsx"
 | |
|         if tag_header:
 | |
|             tags.add("{}:{}".format(tag_header, tag["data"]["value"]))
 | |
|     return sorted(tags)
 | |
| 
 | |
| 
 | |
| def _get_latest_analysis(
 | |
|     clients: Dict[str, nsx_defender.AnalysisClient],
 | |
|     file_hash: str,
 | |
| ) -> Optional[str]:
 | |
|     """
 | |
|     Get the latest analysis.
 | |
| 
 | |
|     :param dict[str, nsx_defender.AnalysisClient] clients: the analysis clients
 | |
|     :param str file_hash: the hash of the file
 | |
|     :rtype: str|None
 | |
|     :return: the task uuid if present, None otherwise
 | |
|     :raises exceptions.ApiError: in case of client errors
 | |
|     :raises exceptions.CommunicationError: in case of client communication errors
 | |
|     """
 | |
|     def _parse_expiration(task_info: Dict[str, str]) -> datetime.datetime:
 | |
|         """
 | |
|         Parse expiration time of a task
 | |
| 
 | |
|         :param dict[str, str] task_info: the task
 | |
|         :rtype: datetime.datetime
 | |
|         :return: the parsed datetime object
 | |
|         """
 | |
|         return datetime.datetime.strptime(task_info["expires"], "%Y-%m-%d %H:%M:%S")
 | |
|     results = []
 | |
|     for data_center, client in clients.items():
 | |
|         response = client.query_file_hash(file_hash=file_hash)
 | |
|         for task in response.get("tasks", []):
 | |
|             results.append(task)
 | |
|     if results:
 | |
|         return sorted(results, key=_parse_expiration)[-1]["task_uuid"]
 | |
|     else:
 | |
|         return None
 | |
| 
 | |
| 
 | |
| def _get_mitre_techniques_galaxy(misp_client: pymisp.PyMISP) -> Dict[str, str]:
 | |
|     """
 | |
|     Get all the MITRE techniques from the MISP galaxy.
 | |
| 
 | |
|     :param pymisp.PyMISP misp_client: the MISP client
 | |
|     :rtype: dict[str, str]
 | |
|     :return: all techniques indexed by their id
 | |
|     """
 | |
|     galaxy_attack_patterns = misp_client.get_galaxy(
 | |
|         galaxy=GALAXY_ATTACK_PATTERNS_UUID,
 | |
|         withCluster=True,
 | |
|         pythonify=True,
 | |
|     )
 | |
|     ret = {}
 | |
|     for cluster in galaxy_attack_patterns.clusters:
 | |
|         ret[cluster.value] = cluster.tag_name
 | |
|     return ret
 | |
| 
 | |
| 
 | |
| def introspection() -> Dict[str, Union[str, List[str]]]:
 | |
|     """
 | |
|     Implement interface.
 | |
| 
 | |
|     :return: the supported MISP attributes
 | |
|     :rtype: dict[str, list[str]]
 | |
|     """
 | |
|     return mispattributes
 | |
| 
 | |
| 
 | |
| def version() -> Dict[str, Union[str, List[str]]]:
 | |
|     """
 | |
|     Implement interface.
 | |
| 
 | |
|     :return: the module config inside another dictionary
 | |
|     :rtype: dict[str, list[str]]
 | |
|     """
 | |
|     moduleinfo["config"] = moduleconfig
 | |
|     return moduleinfo
 | |
| 
 | |
| 
 | |
| def handler(q: Union[bool, str] = False) -> Union[bool, Dict[str, Any]]:
 | |
|     """
 | |
|     Implement interface.
 | |
| 
 | |
|     :param bool|str q: the input received
 | |
|     :rtype: bool|dict[str, any]
 | |
|     """
 | |
|     if q is False:
 | |
|         return False
 | |
| 
 | |
|     request = json.loads(q)
 | |
|     config = request.get("config", {})
 | |
| 
 | |
|     # Load the client to connect to VMware NSX ATA (hard-fail)
 | |
|     try:
 | |
|         analysis_url = config.get("analysis_url")
 | |
|         login_params = {
 | |
|             "key": config["analysis_key"],
 | |
|             "api_token": config["analysis_api_token"],
 | |
|         }
 | |
|         # If 'analysis_url' is specified we are connecting on-premise
 | |
|         if analysis_url:
 | |
|             analysis_clients = {
 | |
|                 DEFAULT_ENDPOINT: nsx_defender.AnalysisClient(
 | |
|                     api_url=analysis_url,
 | |
|                     login_params=login_params,
 | |
|                     verify_ssl=bool(config.get("analysis_verify_ssl", True)),
 | |
|                 )
 | |
|             }
 | |
|             logger.info("Connected NSX AnalysisClient to on-premise infrastructure")
 | |
|         else:
 | |
|             analysis_clients = {
 | |
|                 data_center: nsx_defender.AnalysisClient(
 | |
|                     api_url=tau_clients.NSX_DEFENDER_ANALYSIS_URLS[data_center],
 | |
|                     login_params=login_params,
 | |
|                     verify_ssl=bool(config.get("analysis_verify_ssl", True)),
 | |
|                 ) for data_center in [
 | |
|                     tau_clients.NSX_DEFENDER_DC_WESTUS,
 | |
|                     tau_clients.NSX_DEFENDER_DC_NLEMEA,
 | |
|                 ]
 | |
|             }
 | |
|             logger.info("Connected NSX AnalysisClient to hosted infrastructure")
 | |
|     except KeyError as ke:
 | |
|         logger.error("Integration with VMware NSX ATA failed to connect: %s", str(ke))
 | |
|         return {"error": "Error connecting to VMware NSX ATA: {}".format(ke)}
 | |
| 
 | |
|     # Load the client to connect to MISP (soft-fail)
 | |
|     try:
 | |
|         misp_client = pymisp.PyMISP(
 | |
|             url=config["misp_url"],
 | |
|             key=config["misp_key"],
 | |
|             ssl=bool(config.get("misp_verify_ssl", True)),
 | |
|         )
 | |
|     except (KeyError, pymisp.PyMISPError):
 | |
|         logger.error("Integration with pyMISP disabled: no MITRE techniques tags")
 | |
|         misp_client = None
 | |
| 
 | |
|     # Load the client to connect to VT (soft-fail)
 | |
|     try:
 | |
|         vt_client = vt.Client(apikey=config["vt_key"])
 | |
|     except (KeyError, ValueError):
 | |
|         logger.error("Integration with VT disabled: no automatic download of samples")
 | |
|         vt_client = None
 | |
| 
 | |
|     # Decode and issue the request
 | |
|     try:
 | |
|         if request["attribute"]["type"] == "url":
 | |
|             sample_url = request["attribute"]["value"]
 | |
|             response = analysis_clients[DEFAULT_ENDPOINT].submit_url(sample_url)
 | |
|             task_uuid, tags = _parse_submission_response(response)
 | |
|         else:
 | |
|             if request["attribute"]["type"] == "malware-sample":
 | |
|                 # Raise TypeError
 | |
|                 file_data = _unzip(base64.b64decode(request["attribute"]["data"]))
 | |
|                 file_name = request["attribute"]["value"].split("|", 1)[0]
 | |
|                 hash_value = hashlib.sha1(file_data).hexdigest()
 | |
|             elif request["attribute"]["type"] == "attachment":
 | |
|                 # Raise TypeError
 | |
|                 file_data = base64.b64decode(request["attribute"]["data"])
 | |
|                 file_name = request["attribute"].get("value")
 | |
|                 hash_value = hashlib.sha1(file_data).hexdigest()
 | |
|             else:
 | |
|                 hash_value = request["attribute"]["value"]
 | |
|                 file_data = None
 | |
|                 file_name = "{}.bin".format(hash_value)
 | |
|             # Check whether we have a task for that file
 | |
|             tags = []
 | |
|             task_uuid = _get_latest_analysis(analysis_clients, hash_value)
 | |
|             if not task_uuid:
 | |
|                 # If we have no analysis, download the sample from VT
 | |
|                 if not file_data:
 | |
|                     if not vt_client:
 | |
|                         raise ValueError("No file available locally and VT is disabled")
 | |
|                     file_data = _download_from_vt(vt_client, hash_value)
 | |
|                     tags.append(VT_DOWNLOAD_TAG)
 | |
|                 # ... and submit it (_download_from_vt fails if no sample availabe)
 | |
|                 response = analysis_clients[DEFAULT_ENDPOINT].submit_file(file_data, file_name)
 | |
|                 task_uuid, _tags = _parse_submission_response(response)
 | |
|                 tags.extend(_tags)
 | |
|     except KeyError as e:
 | |
|         logger.error("Error parsing input: %s", request["attribute"])
 | |
|         return {"error": "Error parsing input: {}".format(e)}
 | |
|     except TypeError as e:
 | |
|         logger.error("Error decoding input: %s", request["attribute"])
 | |
|         return {"error": "Error decoding input: {}".format(e)}
 | |
|     except ValueError as e:
 | |
|         logger.error("Error processing input: %s", request["attribute"])
 | |
|         return {"error": "Error processing input: {}".format(e)}
 | |
|     except (exceptions.CommunicationError, exceptions.ApiError) as e:
 | |
|         logger.error("Error issuing API call: %s", str(e))
 | |
|         return {"error": "Error issuing API call: {}".format(e)}
 | |
|     else:
 | |
|         analysis_link = tau_clients.get_task_link(
 | |
|             uuid=task_uuid,
 | |
|             analysis_url=analysis_clients[DEFAULT_ENDPOINT].base,
 | |
|             prefer_load_balancer=True,
 | |
|         )
 | |
| 
 | |
|     # Return partial results if the analysis has yet to terminate
 | |
|     try:
 | |
|         tags.extend(_get_analysis_tags(analysis_clients, task_uuid))
 | |
|         report = analysis_clients[DEFAULT_ENDPOINT].get_result(task_uuid)
 | |
|     except (exceptions.CommunicationError, exceptions.ApiError) as e:
 | |
|         logger.error("Error retrieving the report: %s", str(e))
 | |
|         return {
 | |
|             "results": {
 | |
|                 "types": "link",
 | |
|                 "categories": ["External analysis"],
 | |
|                 "values": analysis_link,
 | |
|                 "tags": tags,
 | |
|             }
 | |
|         }
 | |
| 
 | |
|     # Return the enrichment
 | |
|     try:
 | |
|         techniques_galaxy = None
 | |
|         if misp_client:
 | |
|             techniques_galaxy = _get_mitre_techniques_galaxy(misp_client)
 | |
|         result_parser = ResultParser(techniques_galaxy=techniques_galaxy)
 | |
|         misp_event = result_parser.parse(analysis_link, report)
 | |
|         for tag in tags:
 | |
|             if tag not in frozenset([WORKFLOW_COMPLETE_TAG]):
 | |
|                 misp_event.add_tag(tag)
 | |
|         return {
 | |
|             "results": {
 | |
|                 key: json.loads(misp_event.to_json())[key]
 | |
|                 for key in ("Attribute", "Object", "Tag")
 | |
|                 if (key in misp_event and misp_event[key])
 | |
|             }
 | |
|         }
 | |
|     except pymisp.PyMISPError as e:
 | |
|         logger.error("Error parsing the report: %s", str(e))
 | |
|         return {"error": "Error parsing the report: {}".format(e)}
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     """Main function used to test basic functionalities of the module."""
 | |
|     parser = argparse.ArgumentParser()
 | |
|     parser.add_argument(
 | |
|         "-c",
 | |
|         "--config-file",
 | |
|         dest="config_file",
 | |
|         required=True,
 | |
|         help="the configuration file used for testing",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-t",
 | |
|         "--test-attachment",
 | |
|         dest="test_attachment",
 | |
|         default=None,
 | |
|         help="the path to a test attachment",
 | |
|     )
 | |
|     args = parser.parse_args()
 | |
|     conf = configparser.ConfigParser()
 | |
|     conf.read(args.config_file)
 | |
|     config = {
 | |
|         "analysis_verify_ssl": conf.getboolean("analysis", "analysis_verify_ssl"),
 | |
|         "analysis_key": conf.get("analysis", "analysis_key"),
 | |
|         "analysis_api_token": conf.get("analysis", "analysis_api_token"),
 | |
|         "vt_key": conf.get("vt", "vt_key"),
 | |
|         "misp_url": conf.get("misp", "misp_url"),
 | |
|         "misp_verify_ssl": conf.getboolean("misp", "misp_verify_ssl"),
 | |
|         "misp_key": conf.get("misp", "misp_key"),
 | |
|     }
 | |
| 
 | |
|     # TEST 1: submit a URL
 | |
|     j = json.dumps(
 | |
|         {
 | |
|             "config": config,
 | |
|             "attribute": {
 | |
|                 "type": "url",
 | |
|                 "value": "https://www.google.com",
 | |
|             }
 | |
|         }
 | |
|     )
 | |
|     print(json.dumps(handler(j), indent=4, sort_keys=True))
 | |
| 
 | |
|     # TEST 2: submit a file attachment
 | |
|     if args.test_attachment:
 | |
|         with open(args.test_attachment, "rb") as f:
 | |
|             data = f.read()
 | |
|         j = json.dumps(
 | |
|             {
 | |
|                 "config": config,
 | |
|                 "attribute": {
 | |
|                     "type": "attachment",
 | |
|                     "value": "test.docx",
 | |
|                     "data": base64.b64encode(data).decode("utf-8"),
 | |
|                 }
 | |
|             }
 | |
|         )
 | |
|         print(json.dumps(handler(j), indent=4, sort_keys=True))
 | |
| 
 | |
|     # TEST 3: submit a file hash that is known by NSX ATA
 | |
|     j = json.dumps(
 | |
|         {
 | |
|             "config": config,
 | |
|             "attribute": {
 | |
|                 "type": "md5",
 | |
|                 "value": "002c56165a0e78369d0e1023ce044bf0",
 | |
|             }
 | |
|         }
 | |
|     )
 | |
|     print(json.dumps(handler(j), indent=4, sort_keys=True))
 | |
| 
 | |
|     # TEST 4 : submit a file hash that is NOT known byt NSX ATA
 | |
|     j = json.dumps(
 | |
|         {
 | |
|             "config": config,
 | |
|             "attribute": {
 | |
|                 "type": "sha1",
 | |
|                 "value": "2aac25ecdccf87abf6f1651ef2ffb30fcf732250",
 | |
|             }
 | |
|         }
 | |
|     )
 | |
|     print(json.dumps(handler(j), indent=4, sort_keys=True))
 | |
|     return 0
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     sys.exit(main())
 |