diff --git a/.gitignore b/.gitignore index 539c5dd..a71ada0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /*.json -#/endpoints*.yaml +endpoints*.yaml !examples/*.json !examples/endpoints*.yaml tmp/ diff --git a/endpoints_ise.yaml b/endpoints_ise.yaml new file mode 100644 index 0000000..7954798 --- /dev/null +++ b/endpoints_ise.yaml @@ -0,0 +1,83 @@ +- name: network_device_group + endpoint: /ers/config/networkdevicegroup +- name: allowed_protocols + endpoint: /ers/config/allowedprotocols +- name: device_admin_authorization_global_exception_rule + endpoint: /api/v1/policy/device-admin/policy-set/global-exception +- name: trustsec_security_group + endpoint: /ers/config/sgt +- name: network_access_dictionary + endpoint: /api/v1/policy/network-access/dictionaries +- name: network_access_condition + endpoint: /api/v1/policy/network-access/condition +- name: trustsec_security_group_acl + endpoint: /ers/config/sgacl +- name: allowed_protocols_tacacs + endpoint: /ers/config/allowedprotocols +- name: license_tier_state + endpoint: /api/v1/license/system/tier-state +- name: endpoint_identity_group + endpoint: /ers/config/endpointgroup +- name: certificate_authentication_profile + endpoint: /ers/config/certificateprofile +- name: device_admin_time_and_date_condition + endpoint: /api/v1/policy/device-admin/time-condition +- name: authorization_profile + endpoint: /ers/config/authorizationprofile +- name: network_access_time_and_date_condition + endpoint: /api/v1/policy/network-access/time-condition +- name: identity_source_sequence + endpoint: /ers/config/idstoresequence +- name: repository + endpoint: /api/v1/repository +- name: endpoint + endpoint: /ers/config/endpoint +- name: tacacs_command_set + endpoint: /ers/config/tacacscommandsets +- name: trustsec_ip_to_sgt_mapping + endpoint: /ers/config/sgmapping +- name: user_identity_group + endpoint: /ers/config/identitygroup +- name: trustsec_egress_matrix_cell + endpoint: /ers/config/egressmatrixcell +- name: device_admin_condition + endpoint: /api/v1/policy/device-admin/condition +- name: downloadable_acl + endpoint: /ers/config/downloadableacl +- name: network_access_authorization_global_exception_rule + endpoint: /api/v1/policy/network-access/policy-set/global-exception +- name: internal_user + endpoint: /ers/config/internaluser +- name: network_device + endpoint: /ers/config/networkdevice +- name: trustsec_ip_to_sgt_mapping_group + endpoint: /ers/config/sgmappinggroup +- name: tacacs_profile + endpoint: /ers/config/tacacsprofile +- name: active_directory_join_point + endpoint: /ers/config/activedirectory + children: + - name: active_directory_add_groups + endpoint: /addGroups + - name: active_directory_join_domain_with_all_nodes + endpoint: /joinAllNodes + - name: active_directory_groups_by_domain + endpoint: /getGroupsByDomain +- name: device_admin_policy_set + endpoint: /api/v1/policy/device-admin/policy-set + children: + - name: device_admin_authentication_rule + endpoint: /authentication + - name: device_admin_authorization_exception_rule + endpoint: /exception + - name: device_admin_authorization_rule + endpoint: /authorization +- name: network_access_policy_set + endpoint: /api/v1/policy/network-access/policy-set + children: + - name: network_access_authentication_rule + endpoint: /authentication + - name: network_access_authorization_exception_rule + endpoint: /exception + - name: network_access_authorization_rule + endpoint: /authorization diff --git a/examples/endpoints_catalystcenter.yaml b/examples/endpoints_catalystcenter.yaml new file mode 100644 index 0000000..7a926ec --- /dev/null +++ b/examples/endpoints_catalystcenter.yaml @@ -0,0 +1,125 @@ +- name: transit_network + endpoint: /dna/intent/api/v1/sda/transitNetworks +- name: credentials_snmpv2_write + endpoint: /dna/intent/api/v2/global-credential +# - name: fabric_authentication_profile +# endpoint: /dna/intent/api/v1/business/sda/authentication-profile +# - name: building +# endpoint: /dna/intent/api/v1/site +# - name: fabric_l2_handoff +# endpoint: /dna/intent/api/v1/sda/fabricDevices/layer2Handoffs +# - name: floor +# endpoint: /dna/intent/api/v1/site +# - name: fabric_virtual_network +# endpoint: /dna/intent/api/v1/virtual-network +# - name: role +# endpoint: /dna/system/api/v1/role +# - name: credentials_cli +# endpoint: /dna/intent/api/v2/global-credential +# - name: image_distribution +# endpoint: /dna/intent/api/v1/image/distribution +# - name: discovery +# endpoint: /dna/intent/api/v1/discovery +# - name: wireless_rf_profile +# endpoint: /dna/intent/api/v1/wireless/rf-profile +# - name: area +# endpoint: /dna/intent/api/v1/site +# - name: pnp_device_claim_site +# endpoint: /dna/intent/api/v1/onboarding/pnp-device/site-claim +# - name: credentials_https_write +# endpoint: /dna/intent/api/v2/global-credential +# - name: template_version +# endpoint: /dna/intent/api/v1/template-programmer/template/version +# - name: ip_pool_reservation +# endpoint: /dna/intent/api/v1/reserve-ip-subpool +# - name: ip_pool +# endpoint: /api/v2/ippool +- name: credentials_snmpv2_read + endpoint: /dna/intent/api/v2/global-credential +# - name: pnp_import_devices +# endpoint: /dna/intent/api/v1/onboarding/pnp-device/import +# - name: fabric_device +# endpoint: /dna/intent/api/v1/sda/fabricDevices +# - name: device_role +# endpoint: /dna/intent/api/v1/network-device/brief +# - name: wireless_device_provision +# endpoint: /dna/intent/api/v1/wireless/provision +# - name: device +# endpoint: /dna/intent/api/v1/network-device +# - name: fabric_port_assignment +# endpoint: /dna/intent/api/v1/sda/portAssignments +# - name: sp_profile +# endpoint: /dna/intent/api/v2/service-provider +# - name: network +# endpoint: /dna/intent/api/v2/network +- name: credentials_https_read + endpoint: /dna/intent/api/v2/global-credential +- name: credentials_snmpv3 + endpoint: /dna/intent/api/v2/global-credential +# - name: assign_credentials +# endpoint: /dna/intent/api/v2/credential-to-site +# - name: device_detail +# endpoint: /dna/intent/api/v1/device-detail +# - name: virtual_network_ip_pool +# endpoint: /dna/intent/api/v1/business/sda/virtualnetwork/ippool +# - name: fabric_site +# endpoint: /dna/intent/api/v1/sda/fabricSites +# - name: lan_automation +# endpoint: /dna/intent/api/v1/lan-automation +# - name: user +# endpoint: /dna/system/api/v1/user +# - name: virtual_network_to_fabric_site +# endpoint: /dna/intent/api/v1/business/sda/virtual-network +# - name: wireless_enterprise_ssid +# endpoint: /dna/intent/api/v1/enterprise-ssid +# - name: authentication_policy_server +# endpoint: /dna/intent/api/v1/authentication-policy-servers +# - name: image +# endpoint: /dna/intent/api/v1/image/importation/source/file +# - name: network_profile +# endpoint: /api/v1/siteprofile +# - name: fabric_l3_handoff_ip_transit +# endpoint: /dna/intent/api/v1/sda/fabricDevices/layer3Handoffs/ipTransits +# - name: pnp_config_preview +# endpoint: /dna/intent/api/v1/onboarding/pnp-device/site-config-preview +# - name: image_activation +# endpoint: /dna/intent/api/v1/image/activation/device +# - name: wireless_profile +# endpoint: /intent/api/v1/wirelessProfiles +# - name: fabric_provision_device +# endpoint: /dna/intent/api/v1/sda/provisionDevices +# - name: deploy_template +# endpoint: /dna/intent/api/v2/template-programmer/template/deploy +# - name: pnp_device +# endpoint: /dna/intent/api/v1/onboarding/pnp-device +- name: anycast_gateway + endpoint: /dna/intent/api/v1/sda/anycastGateways +# - name: network_devices +# endpoint: /dna/intent/api/v1/network-device +# - name: site +# endpoint: /dna/intent/api/v1/sites +# children: +# - name: wireless_ssid +# endpoint: /wirelessSettings/ssids +# - name: aaa_settings +# endpoint: /aaaSettings +# - name: project +# endpoint: /dna/intent/api/v1/template-programmer/project +# children: +# - name: template +# endpoint: /template +# - name: +# endpoint: /dna/intent/api/v1/sda/fabrics +# children: +# - name: fabric_vlan_to_ssid +# endpoint: /vlanToSsids +# - name: +# endpoint: /dna/intent/api/v1/networkprofile +# children: +# - name: associate_site_to_network_profile +# endpoint: /site/%v +# - name: tag +# endpoint: /dna/intent/api/v1/tag +# children: +# - name: assign_templates_to_tag +# endpoint: /member diff --git a/nac_collector/cisco_client_catalystcenter.py b/nac_collector/cisco_client_catalystcenter.py new file mode 100644 index 0000000..8c8f53e --- /dev/null +++ b/nac_collector/cisco_client_catalystcenter.py @@ -0,0 +1,299 @@ +import logging + +import click +import requests +import urllib3 + +from nac_collector.cisco_client import CiscoClient + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +logger = logging.getLogger("main") + +# Suppress urllib3 warnings +logging.getLogger("urllib3").setLevel(logging.ERROR) + + +class CiscoClientCATALYSTCENTER(CiscoClient): + """ + This class inherits from the abstract class CiscoClient. It's used for authenticating + with the Cisco Catalyst Center API and retrieving data from various endpoints. + Authentication is username/password based and a session is created upon successful + authentication for subsequent requests. + """ + + DNAC_AUTH_ENDPOINT = "/dna/system/api/v1/auth/token" + SOLUTION = "catalystcenter" + + "Used for mapping credentials to the correct endpoint" + mappings = { + "credentials_snmpv3": "snmpV3", + "credentials_snmpv2_read": "snmpV2cRead", + "credentials_snmpv2_write": "snmpV2cWrite", + "credentials_cli": "cliCredential", + "credentials_https_read": "httpsRead", + "credentials_https_write": "httpsWrite", + } + + def __init__( + self, + username, + password, + base_url, + max_retries, + retry_after, + timeout, + ssl_verify, + ): + super().__init__( + username, password, base_url, max_retries, retry_after, timeout, ssl_verify + ) + + def authenticate(self): + """ + Perform token-based authentication. + + Returns: + bool: True if authentication is successful, False otherwise. + """ + + auth_url = f"{self.base_url}{self.DNAC_AUTH_ENDPOINT}" + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": "application/json", + } + response = requests.post( + auth_url, + auth=(self.username, self.password), + headers=headers, + verify=self.ssl_verify, + timeout=self.timeout, + ) + + if response and response.status_code == 200: + logger.info("Authentication Successful for URL: %s", auth_url) + + token = response.json()["Token"] + + # Create a session after successful authentication + self.session = requests.Session() + self.session.headers.update( + { + "Content-Type": "application/json", + "x-auth-token": token, + } + ) + return True + + logger.error( + "Authentication failed with status code: %s", + response.status_code, + ) + return False + + def process_endpoint_data(self, endpoint, endpoint_dict, data): + """ + Process the data for a given endpoint and update the endpoint_dict. + + Parameters: + endpoint (dict): The endpoint configuration. + endpoint_dict (dict): The dictionary to store processed data. + data (dict or list): The data fetched from the endpoint. + + Returns: + dict: The updated endpoint dictionary with processed data. + """ + + if data is None: + endpoint_dict[endpoint["name"]].append( + {"data": {}, "endpoint": endpoint["endpoint"]} + ) + + # License API returns a list of dictionaries + elif isinstance(data, list): + endpoint_dict[endpoint["name"]].append( + {"data": data, "endpoint": endpoint["endpoint"]} + ) + elif isinstance(data.get("response"), dict): + for k, v in data.get("response").items(): + if self.mappings[endpoint["name"]] == k: + for i in v: + endpoint_dict[endpoint["name"]].append( + { + "data": i, + "endpoint": endpoint["endpoint"] + + "/" + + self.get_id_value(i), + } + ) + elif data.get("response"): + for i in data.get("response"): + endpoint_dict[endpoint["name"]].append( + { + "data": i, + "endpoint": endpoint["endpoint"] + "/" + self.get_id_value(i), + } + ) + + # Pagination for ERS API results + elif data.get("SearchResult"): + ers_data = self.process_ers_api_results(data) + + for i in ers_data: + endpoint_dict[endpoint["name"]].append( + { + "data": i, + "endpoint": endpoint["endpoint"] + "/" + self.get_id_value(i), + } + ) + + return endpoint_dict # Return the processed endpoint dictionary + + def get_from_endpoints(self, endpoints_yaml_file): + """ + Retrieve data from a list of endpoints specified in a YAML file and + run GET requests to download data from controller. + + Parameters: + endpoints_yaml_file (str): The name of the YAML file containing the endpoints. + + Returns: + dict: The final dictionary containing the data retrieved from the endpoints. + """ + + # Load endpoints from the YAML file + logger.info("Loading endpoints from %s", endpoints_yaml_file) + with open(endpoints_yaml_file, "r", encoding="utf-8") as f: + endpoints = self.yaml.load(f) + + # Initialize an empty dictionary + final_dict = {} + + # Iterate over all endpoints + with click.progressbar(endpoints, label="Processing endpoints") as endpoint_bar: + for endpoint in endpoint_bar: + logger.info("Processing endpoint: %s", endpoint["name"]) + + endpoint_dict = CiscoClient.create_endpoint_dict(endpoint) + + data = self.fetch_data(endpoint["endpoint"]) + + # Process the endpoint data and get the updated dictionary + endpoint_dict = self.process_endpoint_data( + endpoint, endpoint_dict, data + ) + + if endpoint.get("children"): + # Create empty list of parent_endpoint_ids + parent_endpoint_ids = [] + + for item in endpoint_dict[endpoint["name"]]: + # Add the item's id to the list + try: + parent_endpoint_ids.append(item["data"]["id"]) + except KeyError: + continue + + for children_endpoint in endpoint["children"]: + logger.info( + "Processing children endpoint: %s", + endpoint["endpoint"] + + "/%v" + + children_endpoint["endpoint"], + ) + + # Iterate over the parent endpoint ids + for id_ in parent_endpoint_ids: + children_endpoint_dict = CiscoClient.create_endpoint_dict( + children_endpoint + ) + + # Replace '%v' in the endpoint with the id + children_joined_endpoint = ( + endpoint["endpoint"] + + "/" + + id_ + + children_endpoint["endpoint"] + ) + + data = self.fetch_data(children_joined_endpoint) + + # Process the children endpoint data and get the updated dictionary + children_endpoint_dict = self.process_endpoint_data( + children_endpoint, children_endpoint_dict, data + ) + + for index, value in enumerate( + endpoint_dict[endpoint["name"]] + ): + if value.get("data").get("id") == id_: + endpoint_dict[endpoint["name"]][index].setdefault( + "children", {} + )[ + children_endpoint["name"] + ] = children_endpoint_dict[ + children_endpoint["name"] + ] + + # Save results to dictionary + final_dict.update(endpoint_dict) + return final_dict + + def process_ers_api_results(self, data): + """ + Process ERS API results and handle pagination. + + Parameters: + data (dict): The data received from the ERS API. + + Returns: + ers_data (list): The processed data. + """ + # Pagination for ERS API results + paginated_data = data["SearchResult"]["resources"] + # Loop through all pages until there are no more pages + while data["SearchResult"].get("nextPage"): + url = data["SearchResult"]["nextPage"]["href"] + # Send a GET request to the URL + response = self.get_request(url) + # Get the JSON content of the response + data = response.json() + paginated_data.extend(data["SearchResult"]["resources"]) + + # For ERS API retrieve details querying all elements from paginated_data + ers_data = [] + for element in paginated_data: + url = element["link"]["href"] + response = self.get_request(url) + # Get the JSON content of the response + data = response.json() + + for _, value in data.items(): + ers_data.append(value) + + return ers_data + + @staticmethod + def get_id_value(i): + """ + Attempts to get the 'id' or 'name' value from a dictionary. + + Parameters: + i (dict): The dictionary to get the 'id' or 'name' value from. + + Returns: + str or None: The 'id' or 'name' value if it exists, None otherwise. + """ + try: + id_value = i["id"] + except KeyError: + try: + id_value = i["rule"]["id"] + except KeyError: + try: + id_value = i["name"] + except KeyError: + id_value = None + + return id_value diff --git a/nac_collector/cli/main.py b/nac_collector/cli/main.py index 93f6e57..8900370 100644 --- a/nac_collector/cli/main.py +++ b/nac_collector/cli/main.py @@ -8,6 +8,7 @@ import nac_collector from nac_collector.cisco_client_fmc import CiscoClientFMC from nac_collector.cisco_client_ise import CiscoClientISE +from nac_collector.cisco_client_catalystcenter import CiscoClientCATALYSTCENTER from nac_collector.cisco_client_ndo import CiscoClientNDO from nac_collector.cisco_client_sdwan import CiscoClientSDWAN from nac_collector.constants import GIT_TMP, MAX_RETRIES, RETRY_AFTER @@ -97,6 +98,8 @@ def main( cisco_client = CiscoClientNDO elif solution == "FMC": cisco_client = CiscoClientFMC + elif solution == "CATALYSTCENTER": + cisco_client = CiscoClientCATALYSTCENTER if cisco_client: client = cisco_client( diff --git a/nac_collector/cli/options.py b/nac_collector/cli/options.py index 6e8d203..75c750b 100644 --- a/nac_collector/cli/options.py +++ b/nac_collector/cli/options.py @@ -4,9 +4,11 @@ solution = click.option( "--solution", "-s", - type=click.Choice(["SDWAN", "ISE", "NDO", "FMC"], case_sensitive=False), + type=click.Choice( + ["SDWAN", "ISE", "NDO", "FMC", "CATALYSTCENTER"], case_sensitive=False + ), required=True, - help="Solutions supported [SDWAN, ISE, NDO, FMC]", + help="Solutions supported [SDWAN, ISE, NDO, FMC, CATALYSTCENTER]", ) username = click.option( diff --git a/nac_collector/github_repo_wrapper.py b/nac_collector/github_repo_wrapper.py index 1749226..91899cb 100644 --- a/nac_collector/github_repo_wrapper.py +++ b/nac_collector/github_repo_wrapper.py @@ -38,10 +38,7 @@ def __init__(self, repo_url, clone_dir, solution): self.logger = logging.getLogger(__name__) self.logger.debug("Initializing GithubRepoWrapper") self._clone_repo() - # Create an instance of the YAML class - # elf.yaml = YAML(typ="safe", pure=True) - # self.yaml.default_flow_style = False - # self.yaml.sort_keys = False + self.yaml = YAML() self.yaml.default_flow_style = False # Use block style self.yaml.indent(sequence=2)