McAfee Active Response (MAR) Search Sample ========================================================= This sample queries McAfee Active Response for the IP addresses of hosts that have an ``Active Response`` client installed. Prerequisites ************* * The samples configuration step has been completed (:doc:`sampleconfig`) * A McAfee Active Response (MAR) Service is available on the DXL fabric * The python client has been authorized to perform MAR searches (see :doc:`marsendauth`) To run this sample execute the ``sample\mar\search_example.py`` script as follows: .. parsed-literal:: c:\\dxlclient-python-sdk-\ |version|\>python sample\\mar\\search_example.py To perform an Active Response query via DXL the following steps must be performed: * Create a MAR search by sending a search query (via a :class:`dxlclient.message.Request` message) to the MAR service * Extract the search identifier from the :class:`dxlclient.message.Response` message received in response to the query creation request * Start the search by sending an appropriate :class:`dxlclient.message.Request` message to the MAR service * Poll the MAR service for the status of the search by sending :class:`dxlclient.message.Request` messages to obtain the search status (via :class:`dxlclient.message.Response` messages) * Once the search has completed, query the MAR service for the results of the search (via a :class:`dxlclient.message.Request` message) * Process the results of the search query in the :class:`dxlclient.message.Response` message received corresponding to the results request All of the MAR query-related :class:`dxlclient.message.Request` messages described in the steps above are sent to the same DXL topic (``/mcafee/mar/service/api/search``). Each of these steps are performed in the sample script and will be described in detail throughout the rest of this document. All of the steps utilize a local convenience method within the script (``execute_mar_search_api()``). This method is responsible for hiding the details of converting Python ``dictionary`` objects to DXL request objects and extracting dictionaries from DXL response objects. The method also hides the details of sending synchronous requests to the MAR service and handling any errors that may occur. The ``execute_mar_search_api()`` method is described in detail at the bottom of this document. The first step is to create a MAR query as shown below. In this particular case the query will be for the IP addresses (``ip_address``) of any hosts (``HostInfo``) that have an Active Response client installed. .. code-block:: python # Create the search response_dict = execute_mar_search_api( client, { "target": "/v1/simple", "method": "POST", "parameters": {}, "body": { "aggregated": True, "projections": [ { "name": "HostInfo", "outputs": ["ip_address"] } ] } } ) As shown below, the code block above results in a :class:`dxlclient.message.Request` message being sent to the MAR service containing the query that is to be created. The :class:`dxlclient.message.Response` message from the MAR service includes meta-information about the search that was created, including a status code (``code``) with a value of 201 (``created``) indicating that the creation was successful along with the identifier (``id``) of the search (this identifier will be used in subsequent steps). .. code-block:: python Request: { "body": { "aggregated": true, "projections": [ { "name": "HostInfo", "outputs": [ "ip_address" ] } ] }, "method": "POST", "parameters": {}, "target": "/v1/simple" } Response: { "body": { "aggregated": true, "catalogVersion": 0, "createdAt": 1474308184842, "dbVersion": 0, "expectedHostResponses": 0, "id": "57e02858e4b0217da8f65e80", "invalid": false, "projections": [ ... ], "running": false, "status": "CREATED", "temporal": true, "ttl": 60000 }, "code": 201 } The next step (as shown below) extracts the identifier (``id``) of the newly created search from the response dictionary. This identifier is included in the next request that is sent to the MAR service requesting that the search be started. .. code-block:: python # Get the search identifier search_id = response_dict["body"]["id"] # Start the search execute_mar_search_api( client, { "target": "/v1/" + search_id + "/start", "method": "PUT", "parameters": {}, "body": {} } ) As shown below, the code block above results in a :class:`dxlclient.message.Request` message being sent to the MAR service requesting that the search be started. The :class:`dxlclient.message.Response` message from the MAR service includes a status code (``code``) with a value of 200 (``OK``) indicating that the search has been started. .. code-block:: python Request: { "body": {}, "method": "PUT", "parameters": {}, "target": "/v1/57e02858e4b0217da8f65e80/start" } Response: { "body": { "aggregated": true, "catalogVersion": 1, "createdAt": 1474308184842, "dbVersion": 2, "executedAt": 1474308184964, "expectedHostResponses": 1, "id": "57e02858e4b0217da8f65e80", "invalid": false, "projections": [ ... ], "running": false, "startTime": 1474308184964, "status": "STARTED", "temporal": true, "ttl": 60000 }, "code": 200 } The next step (as shown below) will poll the MAR service for the status of the executed search until it has reached a status of ``FINISHED``. .. code-block:: python # Wait until the search finishes finished = False while not finished: response_dict = execute_mar_search_api( client, { "target": "/v1/" + search_id + "/status", "method": "GET", "parameters": {}, "body": {} } ) finished = response_dict["body"]["status"] == "FINISHED" if not finished: time.sleep(5) As shown below, the code block above results in one or more :class:`dxlclient.message.Request` messages being sent to the MAR service requesting the status of the search. Each :class:`dxlclient.message.Response` message from the MAR service includes the current status (``status``) of the search (``STARTED``, ``FINISHED``, etc.). .. code-block:: python Request: { "body": {}, "method": "GET", "parameters": {}, "target": "/v1/57e02858e4b0217da8f65e80/status" } Response: { "body": { "errors": 0, "hosts": 0, "results": 0, "status": "STARTED", "subscribedHosts": 0 }, "code": 200 } Request: { "body": {}, "method": "GET", "parameters": {}, "target": "/v1/57e02858e4b0217da8f65e80/status" } Response: { "body": { "errors": 0, "hosts": 1, "results": 1, "status": "FINISHED", "subscribedHosts": 1 }, "code": 200 } Once the search has completed, the next step is to obtain the results of the search from the MAR service (as shown in the code block below). In this particular case, the search results are being limited (``$limit``) to 10 results. .. code-block:: python # Get the search results # Results limited to 10, the API does support paging response_dict = execute_mar_search_api( client, { "target": "/v1/" + search_id + "/results", "method": "GET", "parameters": { "$offset": 0, "$limit": 10, "filter": "", "sortBy": "count", "sortDirection": "desc" }, "body": {} } ) As shown below, the code block above sends a :class:`dxlclient.message.Request` message to the MAR service indicating how results are to be received (filtered, sorted, limited, etc.). The corresponding :class:`dxlclient.message.Response` message includes the search results (``items``) along with meta-information about the results (counts, paging-related information, etc.). .. code-block:: python Request: { "body": {}, "method": "GET", "parameters": { "$limit": 10, "$offset": 0, "filter": "", "sortBy": "count", "sortDirection": "desc" }, "target": "/v1/57e02858e4b0217da8f65e80/results" } Response: { "body": { "currentItemCount": 1, "items": [ { "count": 1, "created_at": "2016-09-19T18:03:07.722Z", "id": "{1=[10.84.200.99]}", "output": { "HostInfo|ip_address": "10.84.200.99" } } ], "itemsPerPage": 10, "startIndex": 0, "totalItems": 1 }, "code": 200 } The final code block in the script extracts the IP addresses from the search results (as shown below). .. code-block:: python # Loop and display the results print("Results:") for result in response_dict['body']['items']: print(" " + result['output']['HostInfo|ip_address']) The output should appear similar to the following: .. code-block:: python Results: 10.84.200.99 The major functionality provided by this sample resides in the ``execute_mar_search_api()`` method as shown below: .. code-block:: python def execute_mar_search_api(client, payload_dict): """ Executes a query against the MAR search api :param client: The DXL client :param payload_dict: The payload :return: A dictionary containing the results of the query """ # Create the request message req = Request(CREATE_SEARCH_TOPIC) # Set the payload req.payload = json.dumps(payload_dict).encode(encoding="UTF-8") # Display the request that is going to be sent print("Request:\n" + json.dumps(payload_dict, sort_keys=True, indent=4, separators=(',', ': '))) # Send the request and wait for a response (synchronous) res = client.sync_request(req, timeout=30) # Return a dictionary corresponding to the response payload if res.message_type != Message.MESSAGE_TYPE_ERROR: resp_dict = json.loads(res.payload.decode(encoding="UTF-8")) # Display the response print("Response:\n" + json.dumps(resp_dict, sort_keys=True, indent=4, separators=(',', ': '))) if "code" in resp_dict: code = resp_dict['code'] if code < 200 or code >= 300: if "body" in resp_dict and "applicationErrorList" in resp_dict["body"]: error = resp_dict["body"]["applicationErrorList"][0] raise Exception(error["message"] + ": " + str(error["code"])) elif "body" in resp_dict: raise Exception(resp_dict["body"] + ": " + str(code)) else: raise Exception("Error: Received failure response code: " + str(code)) else: raise Exception("Error: unable to find response code") return resp_dict else: raise Exception("Error: " + res.error_message + " (" + str(res.error_code) + ")") This method creates a :class:`dxlclient.message.Request` message that will be delivered to the search topic (``/mcafee/mar/service/api/search``) of a MAR service on the fabric. Prior to delivering the request, the dictionary specified as a method parameter (``payload_dict``) is converted to a JSON string and placed in the payload of the request message. The request message is delivered to the fabric via the :func:`dxlclient.client.DxlClient.sync_request` method on the DXL client. The payload of the :class:`dxlclient.message.Response` message received is converted to a Python ``dictionary`` object. The status code (``code``) within the dictionary is examined to ensure that the request was successful. If the request was successful, the dictionary extracted from the response is returned to the caller of the method. The method will raise exceptions for any errors that occur during the request itself or during validation.