[python]Cisco FMC REST API example – GET Server version and add device to Cisco FMC

Introduction

The version used for this lab is Cisco FMC 6.4.0, and Cisco FTD used is 6.3.0. To access the Cisco FMC REST API, you need to ensure it is enabled.
enable rest api

You can test it by going to https:///api/api-explorer if you can see the swagger like documentation then the REST API is enabled, you can test the API here, such as to verify POST request json body, which is useful because the api-explorer is a sandbox… no configuration is actually pushed.

To access the REST API from python code, you need to include X-auth-access-token into the http header, if the request is POST/PUT you need to add Content-Type: "application/json" to the http header in order for the Cisco FMC server to understand your request body if you forgot to do this HTTP 500 internal server error will be returned.

Demonstration

I have uploaded the codes to github, you can access the codes from here. It is quite well documented in my opinion.

This part get the server version, GET request is always simple because there is no request body.
f1

This part adds a FTD device to the Cisco FMC, this requires POST request body. I tested the json body in api explorer before coding it in python, one thing about the api-explorer is that it is a sandbox and can test the post and put request body without actually changing the configuration.
f2
Successful request will have a http 202 ACCEPTED status, this means the request is in progress, it also means the json body is understandable by Cisco FMC.

The response from Cisco FMC will be like this:

{"type":"Device","name":"ftd1 - Firepower Threat '
 'Defense","description":"This is an example to add device to Cisco '
 'FMC","version":"6.0.1","accessPolicy":{"id":"000C29C8-B693-0ed3-0000-017179869187","type":"AccessPolicy"},"hostName":"192.168.100.200","regKey":"cisco","license_caps":["MALWARE","URLFilter","THREAT","BASE"],"keepLocalEvents":false,"ftdMode":"ROUTED","metadata":{"task":{"name":"Device '
 'Registration","id":"ed2bc2f6-58a7-11ea-a748-632f107fcdad","type":"Task"}},"metadata":{"task":{"name":"Device '
 'Registration","id":"ed2bc2f6-58a7-11ea-a748-632f107fcdad","type":"Task"}}}'

f3

Required python library

requests is all you need, this is the best and easiest to use module to work with REST API.
Because my Cisco FMC server does not have a FQDN, I am turning off SSL certification verification requests.post(url, verify=False, headers=headers, data=json.dumps(data)), your production server if it has a SSL certificate that verifies your fqdn you can include your certification path to the verify.
json library is for parsing the dictionary for the request.
json.loads convert string to dictionary.
json.dumps convert dictionary to string, and yes json is “string” in the perception of the server.

from requests.auth import HTTPBasicAuth this library use to get the session token from Cisco FMC server, username and password will be passed to HTTPBasicAuth.

from pprint import pprint this library is to make response easier to read instead of a one line string output to stdout by print function.

from ipaddress import ip_address this library uses ip_address to verify if the string is a valid ipv4 address, if it is invalid a ValueError is raised.

import sys uses sys.exit(1) to exit from the script if there is error.

from getpass import getpass hides the password input by the user of the script.

from typing import Dict, List this is for type hinting. I am learning how to do a properly documented code with python, it is not a must but would be good for your IDE to warn you if data type is not correct it is also easy to understand the function without referring to the entire code.

Function to verify ipv4

This function verifies if a string is a valid ipv4 if it is returns True else return False.

def is_ipv4(ipv4: str) -> bool:
    """
    Check for valid ipv4 address
    :param ipv4:
        test object
    :return:
        if test object is valid ipv4 return True else return False
    """
    try:
        ip_address(ipv4)
        return True
    except ValueError:
        return False

Function to generate a session token

In order to access the REST API you need a X-auth-access-token insert to the http header, this functions returns a dictionary of useful values.

def fmc_gen_token(addr: str = None, username: str = None, password: str = None) -> Dict:
    """
    Generate cisco FMC token
    :param addr:
        address of Cisco FMC
    :param username:
        username of Cisco FMC
    :param password:
        password of Cisco FMC
    :return:
        customized dictionary which contains access token, refresh token and domainUUID
    """
    api_uri = "/api/fmc_platform/v1/auth/generatetoken"
    url = "https://" + addr + api_uri
    response = requests.post(url,
                             verify=False,
                             auth=HTTPBasicAuth(username, password))
    return {
        "X-auth-access-token": response.headers["X-auth-access-token"],
        "X-auth-refresh-token": response.headers["X-auth-refresh-token"],
        "DOMAIN_UUID": response.headers["DOMAIN_UUID"]
    }

Function that get the Cisco FMC version

This function calls the REST API GET request from Cisco FMC to get the FMC version, VDB version, GEODB version and SRU version.

def get_version(addr: str = None, token: str = None):
    """
    Call Cisco FMC REST API for getting server version
    :param addr:
        Cisco FMC address
    :param token:
        X-auth-access-token
    :return:
        HTTP response from Cisco FMC
    """
    api_uri = "/api/fmc_platform/v1/info/serverversion"
    url = "https://" + addr + api_uri
    headers = {
        "X-auth-access-token": token
    }
    response = requests.get(url, verify=False, headers=headers)
    return response

The response is a Response object, response.text is the actual response, response.status_code is the http status such as 200, 202, 500, 404.

If you need to access the keys in response.text use json.loads(response.text) this is because the json response is actually a string, with the loads method it converts to dictionary.

Gets the access policy information

AccessPolicy is a compulsory parameter for the add device body, the id is required in order to POST the request.
The default is id_only=True which returns only the AccessPolicy ID, this code needs to expand as in production you may have more than one AccessPolicy, in my lab only one AccessPolicy hence i did not enumerate the response.

def get_policy_assignment(addr: str = None, token: Dict = None, id_only=True):
    """
    Gets the access policy assignment. This policy is assigned to managed devices.
    :param addr:
        Address of Cisco FMC
    :param token:
        dictionary of token response which includes domainUUID, and X-auth-access-token
    :param id_only:
        Default is True, which gives only access_policy_id, else returns all response.
    :return:
    """
    api_uri = f"/api/fmc_config/v1/domain/{token['DOMAIN_UUID']}/policy/accesspolicies"
    url = "https://" + addr + api_uri
    headers = {
        "X-auth-access-token": token["X-auth-access-token"]
    }
    response = requests.get(url, headers=headers, verify=False)
    if id_only:
        return json.loads(response.text)["items"][0]["id"]
    else:
        return json.loads(response.text)

json.loads(response.text)["items"][0]["id"] this returns the first found id, in reality there could be more items.

Function to add device

This function adds the FTD device to the Cisco FMC.

def add_ftd_device(addr: str = None,
                   name: str = None,
                   hostname: str = None,
                   regkey: str = None,
                   license_caps: List = None,
                   domainuuid: str = None,
                   access_policy_id: str = None,
                   description: str = None,
                   token: str = None):
    """
    Add FTD device to Cisco FMC
    :param description:
        Description of the device to be added.
    :param addr:
        Cisco FMC address
    :param name:
        name of the FTD device
    :param hostname:
        IP address of FTD device
    :param regkey:
        regkey configured in FTD
    :param license_caps:
        PROTECT, CONTROL, URLFilter, MALWARE, BASE
    :param domainuuid:
        DomainUUID from Cisco FMC token header
    :param access_policy_id:
        Access Policy ID assign to FTD
    :param token:
        X-auth-access-token
    :return:
        HTTP response from Cisco FMC
    """
    api_uri = f"/api/fmc_config/v1/domain/{domainuuid}/devices/devicerecords"
    url = "https://" + addr + api_uri
    headers = post_request_headers(token)

    payload = {
        "name": name,
        "hostName": hostname,
        "ftdMode": "ROUTED",
        "description": description,
        "regKey": regkey,
        "type": "Device",
        "license_caps": license_caps,
        "accessPolicy": {
            "id": access_policy_id,
            "type": "AccessPolicy"
        }
    }
    print(payload)
    response = requests.post(url, headers=headers, verify=False, data=json.dumps(payload))
    return response

Function to return POST headers

This is a function to return headers required by POST request.

def post_request_headers(x_auth_access_token: str) -> Dict:
    """
    Lazy function to help to generate request headers
    :param x_auth_access_token:
        X-auth-access-token required by Cisco FMC to access REST API.
    :return:
        dictionary of headers
    """
    return {
        "X-auth-access-token": x_auth_access_token,
        "Content-Type": "application/json"
    }

Function create one host object

This function creates one host firewall object, and yes only one, if you need more than one then the requests needs to be a list of dictionary of objects, and you need to add in bulk parameter to True in the api uri.

def create_host_object(addr: str = None,
                       token: Dict = None,
                       name: str = None,
                       value: str = None,
                       description: str = None):
    """
    Create host object
    :param addr:
        Cisco FMC address
    :param token:
        Token from fmc_gen_token function
    :param name:
        name of the host object
    :param value:
        IP address of the host object
    :param description:
        Description of the host object
    :return:
        response from Cisco FMC
    """
    api_uri = f"/api/fmc_config/v1/domain/{token['DOMAIN_UUID']}/object/hosts"
    url = "https://" + addr + api_uri
    payload = {
        "name": name,
        "type": "Host",
        "value": value,
        "description": description
    }
    headers = post_request_headers(token["X-auth-access-token"])
    response = requests.post(url, verify=False, headers=headers, data=json.dumps(payload))
    return response.text

Demo code

This is a demo function, just to make __main__ neater. This demo code does server version on GET request, and add device on POST request.

def demo():
    """
    Function for demonstration so that __main__ looks neat...
    :return:
    """

    """
    Demonstration code here
    """
    if not is_secret_path_exists(mount_path="cisco_fmc", path="fmc01"):
        """
        Check if vault has FMC's data
        """
        hostname = input("Hostname of FMC: ")
        fmc_ip = input("What is FMC Server ip address: ")
        if not is_ipv4(fmc_ip):
            print("Invalid ip address...bye...")
            sys.exit(1)
        enable_kv2_engine(mount_path="cisco_fmc")
        username = input("Username of fmc01: ")
        password = getpass()
        payload = {
            "username": username,
            "password": password,
            "ip": fmc_ip
        }
        create_update_kv2_secrets(mount_path="cisco_fmc", path=hostname, **payload)

    """
    Codes to get version
    """
    hostname = input("FMC hostname: ")
    data = get_kv2_secret(mount_path="cisco_fmc", path="fmc01", find="data")
    fmc_token = fmc_gen_token(addr=data["ip"],
                              username=data["username"],
                              password=data["password"])
    version = get_version(addr=data["ip"], token=fmc_token["X-auth-access-token"])
    fmc_info = json.loads(version.text)
    print(f"Cisco FMC version:{fmc_info['items'][0]['serverVersion']}")
    print(f"Vulnerable Database version:{fmc_info['items'][0]['vdbVersion']}")
    print(f"Snort rules version:{fmc_info['items'][0]['sruVersion']}")

    # Get Access Policy ID
    policy_id = get_policy_assignment(addr=data["ip"], token=fmc_token)

    """
    Add FTD device to Cisco FMC
    """
    print("*" * 10 + "Add a device to FMC" + "*" * 10)
    device_hostname = input("hostname of FTD: ")
    device_ip_addr = input(f"IP address of {device_hostname}: ")
    regkey = input(f"Registration key configured in {device_hostname}: ")
    description = input(f"Description for this device: ")
    add_device = {
        "addr": data["ip"],
        "name": device_hostname,
        "hostname": device_ip_addr,
        "license_caps": ["MALWARE",
                         "URLFilter",
                         "THREAT",
                         "BASE"],
        "domainuuid": fmc_token["DOMAIN_UUID"],
        "access_policy_id": policy_id,
        "token": fmc_token["X-auth-access-token"],
        "regkey": regkey,
        "description": description
    }
    response = add_ftd_device(**add_device)
    pprint(response.text)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s