From bbf81701a8e85ca5edd880d85c187ce0526a8436 Mon Sep 17 00:00:00 2001 From: akamura Date: Sun, 18 Feb 2024 18:27:54 +0100 Subject: [PATCH] Release 1.3 --- .../akamura/ciscomerakiclu/api/__init__.py | 0 .../ciscomerakiclu/api/meraki_api_manager.py | 74 ++++ .../icons/cisco-meraki-icon.png | Bin 0 -> 27139 bytes .../opt/akamura/ciscomerakiclu/main.py | 191 +++++++++ .../ciscomerakiclu/modules/meraki/__init__.py | 0 .../modules/meraki/meraki_api.py | 387 ++++++++++++++++++ .../modules/meraki/meraki_ms_mr.py | 228 +++++++++++ .../modules/meraki/meraki_mx.py | 148 +++++++ .../akamura/ciscomerakiclu/requirements.txt | 9 + .../opt/akamura/ciscomerakiclu/run | 40 ++ .../ciscomerakiclu/settings/__init__.py | 0 .../ciscomerakiclu/settings/db_creator.py | 97 +++++ .../ciscomerakiclu/settings/term_extra.py | 81 ++++ .../opt/akamura/ciscomerakiclu/setup.py | 61 +++ .../ciscomerakiclu/utilities/__init__.py | 0 .../ciscomerakiclu/utilities/submenu.py | 168 ++++++++ .../akamura/ciscomerakiclu/api/__init__.py | 0 .../ciscomerakiclu/api/meraki_api_manager.py | 76 ++++ .../icons/cisco-meraki-icon.ico | Bin 0 -> 35462 bytes .../WINDOWS/akamura/ciscomerakiclu/main.py | 196 +++++++++ .../ciscomerakiclu/modules/meraki/__init__.py | 0 .../modules/meraki/meraki_api.py | 387 ++++++++++++++++++ .../modules/meraki/meraki_ms_mr.py | 228 +++++++++++ .../modules/meraki/meraki_mx.py | 148 +++++++ .../akamura/ciscomerakiclu/requirements.txt | 9 + .../ciscomerakiclu/settings/__init__.py | 0 .../ciscomerakiclu/settings/db_creator.py | 89 ++++ .../ciscomerakiclu/settings/term_extra.py | 82 ++++ .../WINDOWS/akamura/ciscomerakiclu/setup.py | 61 +++ .../ciscomerakiclu/utilities/__init__.py | 0 .../akamura/ciscomerakiclu/utilities/menu.py | 39 ++ .../ciscomerakiclu/utilities/submenu.py | 168 ++++++++ Source code/WINDOWS/setup.ps1 | 85 ++++ usr/share/applications/ciscomerakiclu.desktop | 7 - 34 files changed, 3052 insertions(+), 7 deletions(-) create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/api/__init__.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/api/meraki_api_manager.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/icons/cisco-meraki-icon.png create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/main.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/__init__.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_api.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_ms_mr.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_mx.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/requirements.txt create mode 100755 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/run create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/__init__.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/db_creator.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/term_extra.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/setup.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/utilities/__init__.py create mode 100644 Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/utilities/submenu.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/api/__init__.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/api/meraki_api_manager.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/icons/cisco-meraki-icon.ico create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/main.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/__init__.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_api.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_ms_mr.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_mx.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/requirements.txt create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/settings/__init__.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/settings/db_creator.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/settings/term_extra.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/setup.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/utilities/__init__.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/utilities/menu.py create mode 100644 Source code/WINDOWS/akamura/ciscomerakiclu/utilities/submenu.py create mode 100644 Source code/WINDOWS/setup.ps1 delete mode 100644 usr/share/applications/ciscomerakiclu.desktop diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/api/__init__.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/api/meraki_api_manager.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/api/meraki_api_manager.py new file mode 100644 index 0000000..8ba8977 --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/api/meraki_api_manager.py @@ -0,0 +1,74 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +from pysqlcipher3 import dbapi2 as sqlite +from termcolor import colored + + +# ================================================== +# SAVE the Cisco Meraki API Key in the Encrypted DB +# ================================================== +def save_api_key(db_password, api_key): + try: + db_path = os.path.join('/opt/akamura/ciscomerakiclu/db', 'cisco_meraki_clu_db.db') + conn = sqlite.connect(db_path) + conn.execute(f"PRAGMA key = '{db_password}'") + conn.execute("INSERT OR REPLACE INTO sensitive_data (id, data) VALUES (1, ?)", (api_key,)) + conn.commit() + print("API key saved successfully.") + except Exception as e: + print(f"An error occurred: {e}") + finally: + conn.close() + +def get_api_key(db_password): + conn = None # Initialize connection to None + try: + db_path = os.path.join('/opt/akamura/ciscomerakiclu/db', 'cisco_meraki_clu_db.db') + conn = sqlite.connect(db_path) + conn.execute(f"PRAGMA key = '{db_password}'") + + cursor = conn.execute("SELECT data FROM sensitive_data WHERE id = 1") + api_key = cursor.fetchone() + + return api_key[0] if api_key else None + + except Exception as e: + print(colored("An error occurred while accessing the database: ", "red") + str(e)) + return None + + finally: + if conn: + conn.close() \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/icons/cisco-meraki-icon.png b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/icons/cisco-meraki-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..41c1319ad78d65c8d75a4258178d76a956fb0fec GIT binary patch literal 27139 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 literal 0 HcmV?d00001 diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/main.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/main.py new file mode 100644 index 0000000..eb9dd61 --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/main.py @@ -0,0 +1,191 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +import sys +import logging +import traceback + +required_packages = { + "tabulate": "tabulate", + "pathlib": "pathlib", + "datetime": "datetime", + "termcolor": "termcolor", + "requests": "requests", + "rich": "rich", + "setuptools": "setuptools", + "pysqlcipher3": "pysqlcipher3" +} + +missing_packages = [] +for module, package in required_packages.items(): + try: + __import__(module) + except ImportError: + missing_packages.append(package) + +if missing_packages: + print("Missing required Python packages: " + ", ".join(missing_packages)) + print("Please install them using the following command:") + print(f"{sys.executable} -m pip install " + " ".join(missing_packages)) + sys.exit(1) + +from getpass import getpass +from datetime import datetime +from termcolor import colored +from pysqlcipher3 import dbapi2 as sqlite + + +# ================================================== +# IMPORT custom modules +# ================================================== +from api import meraki_api_manager +from settings import term_extra +from settings import db_creator +from utilities import submenu + + +# ================================================== +# ERROR handling and logging +# ================================================== +logger = logging.getLogger('ciscomerakiclu') +logger.setLevel(logging.ERROR) + +log_directory = '/opt/akamura/ciscomerakiclu/log/' +if not os.path.exists(log_directory): + os.makedirs(log_directory) + +log_file = os.path.join(log_directory, 'error.log') +file_handler = logging.FileHandler(log_file) +file_handler.setLevel(logging.ERROR) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +file_handler.setFormatter(formatter) +logger.addHandler(file_handler) + + +# ================================================== +# VISUALIZE the Main Menu +# ================================================== +def main_menu(db_password): + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + + api_key = meraki_api_manager.get_api_key(db_password) + options = [ + "Network wide [under dev]", + "Security & SD-WAN", + "Switch and wireless", + "Environmental [under dev]", + "Organization [under dev]", + "The Swiss Army Knife [under dev]", + f"{'Edit Cisco Meraki API Key' if api_key else 'Set Cisco Meraki API Key'}", + "Exit the Command Line Utility" + ] + current_year = datetime.now().year + footer = f"\033[1mPROJECT PAGE\033[0m\n© {current_year} Matia Zanella\nhttps://developer.cisco.com/codeexchange/github/repo/akamura/cisco-meraki-clu/\n\n\033[1mSUPPORT ME\033[0m\n☕️ Fuel me with a coffee if you found it useful https://www.paypal.com/paypalme/matiazanella/\n\n\033[1mDISCLAIMER\033[0m\nThis utility is not an official Cisco Meraki product but is based on the official Cisco Meraki API.\nIt is intended to provide Network Administrators with an easy daily companion in the swiss army knife." + + # Description header over the menu + print("\n") + print("┌" + "─" * 58 + "┐") + print("│".ljust(59) + "│") + for index, option in enumerate(options, start=1): + print(f"│ {index}. {option}".ljust(59) + "│") + print("│".ljust(59) + "│") + print("└" + "─" * 58 + "┘") + + term_extra.print_footer(footer) + choice = input(colored("Choose a menu option [1-8]: ", "cyan")) + + if choice.isdigit() and 1 <= int(choice) <= 8: + if choice == '2': + if api_key: + submenu.submenu_mx(api_key) + else: + print("Please set the Cisco Meraki API key first.") + input(colored("\nPress Enter to return to the main menu...", "green")) + + elif choice == '3': + if api_key: + submenu.submenu_sw_and_ap(api_key) + else: + print("Please set the Cisco Meraki API key first.") + input(colored("\nPress Enter to return to the main menu...", "green")) + + elif choice == '7': + manage_api_key(db_password) + elif choice == '8': + term_extra.clear_screen() + term_extra.print_ascii_art() + + print("\nThank you for using the Cisco Meraki Command Line Utility!") + print("Exiting the program. Goodbye, and have a wonderful day!") + print("\n🚀 \033[1mCONTRIBUTE\033[0m\nThis is not just a project; it's a community effort.\nI'm inviting you to be a part of this journey.\nStar it, fork it, contribute, or just play around with it.\nEvery feedback, issue, or pull request is an opportunity for us to make this tool even more amazing.\nYou are more than welcome to discuss it on GitHub https://github.com/akamura/cisco-meraki-clu/discussions") + print("\n" * 2) + + break + else: + print(colored(f"You selected: {options[int(choice) - 1]}", "green")) + else: + print(colored("Invalid choice. Please try again.", "red")) + + +def manage_api_key(db_password): + term_extra.clear_screen() + api_key = input("\nEnter the Cisco Meraki API Key: ") + meraki_api_manager.save_api_key(db_password, api_key) + + +# ================================================== +# ERROR handling and logging +# ================================================== +if __name__ == "__main__": + try: + db_password = "" + if not db_creator.database_exists(): + os.system('clear') + term_extra.print_ascii_art() + db_password = db_creator.prompt_create_database() + else: + os.system('clear') + term_extra.print_ascii_art() + db_password = getpass(colored("\n\nWelcome to Cisco Meraki Command Line Utility!\nThis program contains sensitive information. Please insert your password to continue: ", "green")) + db_creator.verify_database_password(db_password) + main_menu(db_password) + except Exception as e: + logger.error("An error occurred", exc_info=True) + print("An error occurred:") + print(e) + traceback.print_exc() + input("\nPress Enter to exit.\n") \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/__init__.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_api.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_api.py new file mode 100644 index 0000000..475cd5b --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_api.py @@ -0,0 +1,387 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import requests +import subprocess +import sys +import csv +import os +try: + from tabulate import tabulate +except ImportError: + subprocess.check_call([sys.executable, "-m", "pip", "install", "tabulate"]) +from datetime import datetime +try: + from termcolor import colored +except ImportError: + subprocess.check_call([sys.executable, "-m", "pip", "install", "termcolor"]) + + +# ================================================== +# EXPORT device list in a beautiful table format +# ================================================== +def export_devices_to_csv(devices, network_name, device_type, base_folder_path): + current_date = datetime.now().strftime("%Y-%m-%d") + filename = f"{network_name}_{current_date}_{device_type}.csv" + file_path = os.path.join(base_folder_path, filename) + + if devices: + # Priority columns + priority_columns = ['name', 'mac', 'lanIp', 'serial', 'model', 'firwmare', 'tags'] + + # Gather all columns from the devices + all_columns = set(key for device in devices for key in device.keys()) + + # Reorder columns so that priority columns come first + ordered_columns = priority_columns + [col for col in all_columns if col not in priority_columns] + + with open(file_path, 'w', newline='', encoding='utf-8') as file: + # Convert fieldnames to uppercase + fieldnames = [col.upper() for col in ordered_columns] + writer = csv.DictWriter(file, fieldnames=fieldnames) + writer.writeheader() + for device in devices: + # Convert keys to uppercase to match fieldnames + row = {col.upper(): device.get(col, '') for col in ordered_columns} + writer.writerow(row) + + print(f"Data exported to {file_path}") + else: + print("No data to export.") + + +# ================================================== +# EXPORT firewall rules in a beautiful table format +# ================================================== +def export_firewall_rules_to_csv(firewall_rules, network_name, base_folder_path): + current_date = datetime.now().strftime("%Y-%m-%d") + filename = f"{network_name}_{current_date}_MX_Firewall_Rules.csv" + file_path = os.path.join(base_folder_path, filename) + + if firewall_rules: + # Priority columns, adjust these based on your data + priority_columns = ['policy', 'protocol', 'srcport', 'srccidr', 'destport','destcidr','comments'] + + # Gather all columns from the firewall rules + all_columns = set(key for rule in firewall_rules for key in rule.keys()) + + # Reorder columns so that priority columns come first + ordered_columns = priority_columns + [col for col in all_columns if col not in priority_columns] + + with open(file_path, 'w', newline='', encoding='utf-8') as file: + # Convert fieldnames to uppercase + fieldnames = [col.upper() for col in ordered_columns] + writer = csv.DictWriter(file, fieldnames=fieldnames) + writer.writeheader() + for rule in firewall_rules: + # Convert keys to uppercase to match fieldnames + row = {col.upper(): rule.get(col, '') for col in ordered_columns} + writer.writerow(row) + + print(f"Data exported to {file_path}") + else: + print("No data to export.") + + +# ================================================== +# GET a list of Organizations +# ================================================== +def get_meraki_organizations(api_key): + url = "https://api.meraki.com/api/v1/organizations" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json" + } + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print("Failed to fetch organizations") + return None + +def select_organization(api_key): + organizations = get_meraki_organizations(api_key) + if organizations: + for idx, org in enumerate(organizations, 1): + print(f"{idx}. {org['name']}") + + choice = input(colored("\nSelect an Organization (enter the number): ", "cyan")) + try: + selected_index = int(choice) - 1 + if 0 <= selected_index < len(organizations): + return organizations[selected_index] + else: + print("Invalid selection.") + except ValueError: + print("Please enter a number.") + + return None + + +# ================================================== +# GET a list of Networks in an Organization +# ================================================== +def get_meraki_networks(api_key, organization_id, per_page=5000): + url = f"https://api.meraki.com/api/v1/organizations/{organization_id}/networks" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json", + "Accept": "application/json" + } + params = { + "perPage": per_page + } + response = requests.get(url, headers=headers, params=params) + if response.status_code == 200: + networks = response.json() + # Sort the networks by name + networks.sort(key=lambda x: x['name']) + return networks + else: + print("Failed to fetch networks") + return None, None + + +# ================================================== +# SELECT a Network in an Organization +# ================================================== +def select_network(api_key, organization_id): + networks = get_meraki_networks(api_key, organization_id) + if networks: + for idx, network in enumerate(networks, 1): + print(f"{idx}. {network['name']}") + + choice = input(colored("\nSelect an Organization Network (enter the number): ", "cyan")) + try: + selected_index = int(choice) - 1 + if 0 <= selected_index < len(networks): + return networks[selected_index] + else: + print("Invalid selection.") + except ValueError: + print("Please enter a number.") + + return None + + +# ================================================== +# GET a list of Switches in an Network +# ================================================== +def get_meraki_switches(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/devices" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + devices = response.json() + switches = [device for device in devices if device['model'].startswith('MS')] + return switches + else: + print("Failed to fetch switches") + return None + +def display_switches(api_key, network_id): + switches = get_meraki_switches(api_key, network_id) + if switches: + print(tabulate(switches, headers="keys", tablefmt="pretty")) + else: + print("No switches found in the selected network.") + + +# ================================================== +# GET a list of Switch Ports and their Status +# ================================================== +def get_switch_ports(api_key, serial): + url = f"https://api.meraki.com/api/v1/devices/{serial}/switch/ports" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch switch ports for serial {serial}, status code: {response.status_code}") + return None + +def get_switch_ports_statuses_with_timespan(api_key, serial, timespan=1800): + url = f"https://api.meraki.com/api/v1/devices/{serial}/switch/ports/statuses" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + # Assuming the API supports a 'timespan' query parameter for this endpoint + params = {'timespan': timespan} + + response = requests.get(url, headers=headers, params=params) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch switch ports for serial {serial}, status code: {response.status_code}") + return None + + +# ================================================== +# GET a list of Access Points in an Network +# ================================================== +def get_meraki_access_points(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/devices" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + devices = response.json() + # Filter to include only access points (APs) + access_points = [device for device in devices if device['model'].startswith('MR')] + return access_points + else: + print("Failed to fetch access points") + return None + + +# ======================================================================= +# [UNDER DEVELOPMENT] GET a list of VLANs and Static Routes in an Network +# ======================================================================= +def get_meraki_vlans(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/vlans" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch VLANs: {response.status_code}, {response.text}") + return None + +def get_meraki_static_routes(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/staticRoutes" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch static routes: {response.status_code}, {response.text}") + return None + + +# ================================================== +# SELECT a Network in an Organization +# ================================================== +def select_mx_network(api_key, organization_id): + networks = get_meraki_networks(api_key, organization_id) + if networks: + for idx, network in enumerate(networks, 1): + print(f"{idx}. {network['name']}") + + choice = input(colored("\nSelect an Organization Network (enter the number): ", "cyan")) + try: + selected_index = int(choice) - 1 + if 0 <= selected_index < len(networks): + return networks[selected_index] + else: + print("Invalid selection.") + except ValueError: + print("Please enter a number.") + + return None + + +# ================================================== +# GET Layer 3 Firewall Rules for a Network +# ================================================== +def get_l3_firewall_rules(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/appliance/firewall/l3FirewallRules" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json()["rules"] + else: + print("Failed to fetch L3 firewall rules") + return None + + +# ================================================== +# DISPLAY Firewall Rules in a Table Format +# ================================================== +def display_firewall_rules(firewall_rules): + if firewall_rules: + print("\nLayer 3 Firewall Rules:") + print(tabulate(firewall_rules, headers="keys", tablefmt="pretty")) + else: + print("No firewall rules found in the selected network.") + + +# ============================================================== +# FETCH Organization policy and group objects for Firewall Rules +# ============================================================== +def get_organization_policy_objects(api_key, organization_id): + url = f"https://api.meraki.com/api/v1/organizations/{organization_id}/policyObjects" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json", + "Accept": "application/json" + } + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + print("Policy Objects:", data[:5]) + return data + else: + print(f"Failed to fetch organization policy objects: {response.text}") + return [] + +def get_organization_policy_objects_groups(api_key, organization_id): + url = f"https://api.meraki.com/api/v1/organizations/{organization_id}/policyObjects/groups" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json", + "Accept": "application/json" + } + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + print("Policy Objects Groups:", data[:5]) + return data + else: + print(f"Failed to fetch organization policy objects groups: {response.text}") + return [] + + +# ============================================================== +# FETCH Organization Devices Statuses +# ============================================================== +def get_organization_devices_statuses(api_key, organization_id): + url = f"https://api.meraki.com/api/v1/organizations/{organization_id}/devices/statuses" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json" + } + + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch organization devices statuses. Status code: {response.status_code}") + return [] \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_ms_mr.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_ms_mr.py new file mode 100644 index 0000000..4e9cb0d --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_ms_mr.py @@ -0,0 +1,228 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +from datetime import datetime +from termcolor import colored +from rich.console import Console +from rich.table import Table +from rich.box import SIMPLE + + +# ================================================== +# IMPORT custom modules +# ================================================== +from modules.meraki import meraki_api +from settings import term_extra + + +# ================================================== +# DEFINE how to retrieve Switch Ports data +# ================================================== +def display_switch_ports(api_key, serial_number): + port_statuses = [] + + try: + switch_ports = meraki_api.get_switch_ports(api_key, serial_number) + except Exception as e: + print(f"[red]Failed to fetch switch port configurations: {e}[/red]") + return + + try: + port_statuses = meraki_api.get_switch_ports_statuses_with_timespan(api_key, serial_number) or [] + except Exception as e: + print(f"[red]Failed to fetch real-time port statuses/packets: {e}[/red]") + + if switch_ports: + table = Table(show_header=True, header_style="green", box=SIMPLE) + + columns = [ + ("Port", 5), ("Name", 30), ("Enabled", 5), + ("PoE", 5), ("Type", 10), ("VLAN", 5), + ("Allowed VLANs", 8), ("RSTP", 5), ("STP Guard", 10), + ("Storm Cont", 5), ("In (Gbps)", 8), ("Out (Gbps)", 8), + ("powerUsageInWh", 8), ("warnings", 30), ("errors", 30) + ] + for col_name, col_width in columns: + table.add_column(col_name, style="dim", width=col_width) + + for port in switch_ports: + port_id = port.get('portId', 'N/A') + status = next((item for item in port_statuses if item.get("portId") == port_id), {}) + + row_data = [ + port.get('portId', 'N/A'), + port.get('name', 'N/A'), + "Yes" if port.get('enabled') else "No", + "Yes" if port.get('poeEnabled') else "No", + port.get('type', 'N/A'), + str(port.get('vlan', 'N/A')), + port.get('allowedVlans', 'N/A'), + "Yes" if port.get('rstpEnabled') else "No", + port.get('stpGuard', 'N/A'), + "Yes" if port.get('stormControlEnabled') else "No", + f"{float(status.get('usageInKb', {}).get('recv', 'N/A')) / 1000000:.2f}" if status.get('usageInKb', {}).get('recv', 'N/A') != 'N/A' else 'N/A', + f"{float(status.get('usageInKb', {}).get('sent', 'N/A')) / 1000000:.2f}" if status.get('usageInKb', {}).get('sent', 'N/A') != 'N/A' else 'N/A', + str(status.get('powerUsageInWh', 'N/A')), + str(status.get('warnings', 'N/A')), + str(status.get('errors', 'N/A')) + ] + table.add_row(*row_data) + + console = Console() + console.print("\nSwitch Ports:") + console.print(table) + else: + print("[red]No ports found for the given serial number or failed to fetch ports.[/red]") + + input("Press Enter to continue...") + + +# ================================================== +# DISPLAY device list in a beautiful table format +# ================================================== +def display_devices(api_key, network_id, device_type): + devices = None + if device_type == 'switches': + devices = meraki_api.get_meraki_switches(api_key, network_id) + elif device_type == 'access_points': + devices = meraki_api.get_meraki_access_points(api_key, network_id) + + term_extra.clear_screen() + term_extra.print_ascii_art() + + if devices: + devices = sorted(devices, key=lambda x: x.get('name', '').lower()) + table = Table(show_header=True, header_style="green", box=SIMPLE) + + priority_columns = ['name', 'mac', 'lanIp', 'serial', 'model'] + excluded_columns = ['networkId', 'details', 'lat', 'lng', 'firmware'] + other_columns = [key for key in devices[0].keys() if key not in priority_columns and key not in excluded_columns] + + for key in priority_columns: + table.add_column(key.upper(), no_wrap=True) + + for key in other_columns: + table.add_column(key.upper(), no_wrap=False) + + for device in devices: + row_data = [str(device.get(key, "")) for key in priority_columns + other_columns] + table.add_row(*row_data) + + console = Console() + console.print(table) + else: + print(colored(f"No {device_type} found in the selected network.", "red")) + + choice = input(colored("\nPress Enter to return to the precedent menu...", "green")) + + +# ================================================== +# DISPLAY organization devices statuses in table +# ================================================== +def display_organization_devices_statuses(api_key, organization_id, network_id): + devices_statuses = meraki_api.get_organization_devices_statuses(api_key, organization_id) + term_extra.clear_screen() + term_extra.print_ascii_art() + + filtered_devices_statuses = [device for device in devices_statuses if device.get('networkId') == network_id] + filtered_devices_statuses = [device for device in filtered_devices_statuses if device.get('productType') in ["switch", "wireless"]] + + if devices_statuses: + devices_statuses = [device for device in devices_statuses if device.get('productType') in ["switch", "wireless"]] + devices_statuses = sorted(devices_statuses, key=lambda x: x.get('name', '').lower()) + table = Table(show_header=True, header_style="green", box=SIMPLE) + + priority_columns = ['name', 'serial', 'mac', 'ipType', 'lanIp', 'gateway', 'primaryDns', 'secondaryDns', 'PSU 1', 'PSU 2', 'status', 'lastReportedAt'] + + for key in priority_columns: + formatted_key = key if not key.startswith("PSU") else key.replace(" ", "") + table.add_column(formatted_key.upper(), no_wrap=True) + + for device in devices_statuses: + row_data = [] + for key in priority_columns[:-4]: + value = str(device.get(key, "N/A")) + row_data.append(value) + + add_power_supply_statuses(device, row_data) + + status_value = str(device.get('status', "N/A")) + if status_value.lower() == 'online': + row_data.append(f"[green]{status_value}[/green]") + elif status_value.lower() == 'dormant': + row_data.append(f"[yellow]{status_value}[/yellow]") + elif status_value.lower() == 'offline' or status_value.lower() == 'alerting': + row_data.append(f"[red]{status_value}[/red]") + else: + row_data.append(status_value) + + last_reported_at = device.get('lastReportedAt') + if last_reported_at: + original_datetime = datetime.strptime(last_reported_at, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_datetime = original_datetime.strftime("%Y-%m-%d %H:%M") + row_data.append(formatted_datetime) + else: + row_data.append("N/A") + + table.add_row(*row_data) + + console = Console() + console.print(table) + + else: + print("[red]No 'switch' devices found in the selected network.[/red]") + choice = input("\nPress Enter to return to the previous menu... ") + + +# ================================================== +# INTEGRATE additional JSON data for PSU's details +# ================================================== +def add_power_supply_statuses(device, row_data): + power_statuses = [] + if 'components' in device and 'powerSupplies' in device['components']: + for slot in [1, 2]: + power_supply = next((ps for ps in device['components']['powerSupplies'] if ps.get('slot') == slot), None) + if power_supply: + status = power_supply.get('status', 'N/A') + if status.lower() == 'powering': + power_statuses.append(f"[green]{status}[/green]") + elif status.lower() == 'disconnected': + power_statuses.append(f"[red]{status}[/red]") + else: + power_statuses.append(status) + else: + power_statuses.append("N/A") + else: + power_statuses = ["N/A", "N/A"] + row_data.extend(power_statuses) \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_mx.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_mx.py new file mode 100644 index 0000000..9b83d13 --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/modules/meraki/meraki_mx.py @@ -0,0 +1,148 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +import sys + +from pathlib import Path +from datetime import datetime +from termcolor import colored +from rich.console import Console +from rich.table import Table +from rich.box import SIMPLE +from rich.text import Text + + +# ================================================== +# IMPORT custom modules +# ================================================== +from modules.meraki import meraki_api +from settings import term_extra + + +# ================================================== +# DISPLAY Firewall Rules in a Beautiful Table Format +# ================================================== +def display_firewall_rules(api_key, network_id, organization_id): + rules = meraki_api.get_l3_firewall_rules(api_key, network_id) + policy_objects = meraki_api.get_organization_policy_objects(api_key, organization_id) + policy_objects_groups = meraki_api.get_organization_policy_objects_groups(api_key, organization_id) + + # Create mappings for objects and groups + obj_mapping = {str(obj['id']): obj['name'] for obj in policy_objects} + group_mapping = {str(group['id']): group['name'] for group in policy_objects_groups} + + term_extra.clear_screen() + term_extra.print_ascii_art() + + if rules: + table = Table(show_header=True, header_style="green", box=SIMPLE) + priority_columns = ['policy', 'protocol', 'srcPort', 'srcCidr', 'destPort', 'destCidr'] + excluded_columns = ['syslogEnabled'] + other_columns = [key for key in rules[0].keys() if key not in priority_columns and key not in excluded_columns] + + for key in priority_columns + other_columns: + table.add_column(key.upper(), no_wrap=False) + + for rule in rules: + for cidr_field in ['srcCidr', 'destCidr']: + original_cidr = rule[cidr_field] + + if original_cidr.startswith("OBJ(") or original_cidr.startswith("GRP("): + obj_or_grp_id = original_cidr[4:-1] + rule[cidr_field] = obj_mapping.get(obj_or_grp_id, group_mapping.get(obj_or_grp_id, original_cidr)) + + row_data = [str(rule.get(key, "")) for key in priority_columns + other_columns] + policy = rule.get("policy", "").lower() + row_style = "green" if policy == "allow" else "red" if policy == "deny" else "" + styled_row_data = [Text(cell, style=row_style) for cell in row_data] + table.add_row(*styled_row_data) + + console = Console() + console.print(table) + else: + print(colored("No firewall rules found in the selected network.", "red")) + input(colored("\nPress Enter to return to the previous menu...", "green")) + + + + +# ================================================== +# PROCESS Data Inside Networks (MX Firewall Rules) +# ================================================== +def select_mx_network(api_key, organization_id): + selected_network = meraki_api.select_mx_network(api_key, organization_id) + if selected_network: + network_name = selected_network['name'] + network_id = selected_network['id'] + + display_firewall_rules(api_key, network_id, organization_id) + + downloads_path = str(Path.home() / "Downloads") + current_date = datetime.now().strftime("%Y-%m-%d") + meraki_dir = os.path.join(downloads_path, f"Cisco-Meraki-CLU-Export-{current_date}") + os.makedirs(meraki_dir, exist_ok=True) + + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + header = "MX Firewall Rules Menu" + options = [ + "List Firewall Rules", + "Download Firewall Rules CSV", + "Status (under dev)", + "Return to Main Menu" + ] + term_extra.print_header(header) + term_extra.print_menu(options) + + columns, _ = term_extra.get_terminal_size() + print("-" * columns) + + choice = input(colored("\nChoose a menu option [1-4]: ", "cyan")) + + if choice == '1': + display_firewall_rules(api_key, network_id) + + elif choice == '2': + firewall_rules = meraki_api.get_l3_firewall_rules(api_key, network_id) + + if firewall_rules: + meraki_api.export_firewall_rules_to_csv(firewall_rules, network_name, meraki_dir) + else: + print("No firewall rules to download.") + choice = input(colored("\nPress Enter to return to the precedent menu...", "green")) + + elif choice == '4': + break \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/requirements.txt b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/requirements.txt new file mode 100644 index 0000000..f470fb1 --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/requirements.txt @@ -0,0 +1,9 @@ +# Required dependencies to run Cisco Meraki CLU +tabulate +pathlib +datetime +termcolor +requests +pysqlcipher3 +rich +setuptools \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/run b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/run new file mode 100755 index 0000000..81e86a2 --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/run @@ -0,0 +1,40 @@ +#!/bin/bash + +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# Change Terminal geometry and clear the screen +# ================================================== +printf '\e[8;48;150t' +clear + +python3 /opt/akamura/ciscomerakiclu/main.py \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/__init__.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/db_creator.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/db_creator.py new file mode 100644 index 0000000..56b18eb --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/db_creator.py @@ -0,0 +1,97 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +from getpass import getpass +from termcolor import colored +from pysqlcipher3 import dbapi2 as sqlite + +# ================================================== +# CREATE the encrypted Database +# ================================================== +def create_cisco_meraki_clu_db(password): + db_path = '/opt/akamura/ciscomerakiclu/db/cisco_meraki_clu_db.db' + conn = None + + try: + if not os.path.exists(os.path.dirname(db_path)): + os.makedirs(os.path.dirname(db_path)) + + conn = sqlite.connect(db_path) + conn.execute(f"PRAGMA key = '{password}'") + conn.execute("CREATE TABLE IF NOT EXISTS sensitive_data (id INTEGER PRIMARY KEY, data TEXT)") + print("Database and table created successfully.") + conn.close() + return True + except Exception as e: + print(colored("\nFailed to create or access the encrypted database.\n", "red") + str(e)) + input("\nPress Enter to retry") + return False + + finally: + if conn: + conn.close() + +def database_exists(): + db_path = '/opt/akamura/ciscomerakiclu/db/cisco_meraki_clu_db.db' + return os.path.exists(db_path) + +def verify_database_password(password): + try: + db_path = os.path.join('/opt/akamura/ciscomerakiclu/db', 'cisco_meraki_clu_db.db') + conn = sqlite.connect(db_path) + conn.execute(f"PRAGMA key = '{password}'") + conn.execute("SELECT count(*) FROM sensitive_data") + conn.close() + except Exception as e: + print(colored("\nError: The provided database password is incorrect.\n", "red")) + input("\nPress Enter to close the program") + return False + +def prompt_create_database(): + create_db = input("The program need a SQLCipher encrypted database to store sensitive data like Cisco Meraki API key.\nDo you want to create the DB? [yes - no]]: ").strip().lower() + if create_db == 'yes': + print(colored("\nREMEMBER TO SAVE YOUR DATABASE PASSWORD IN A SAFE PLACE!", "red")) + print(colored("YOU WILL NEED IT TO ACCESS THE APPLICATION!\n", "red")) + db_password = getpass("Enter the encryption password for the database: ") + create_cisco_meraki_clu_db(db_password) + return db_password + elif create_db == 'no': + print(colored("\nNo database created. Exiting the program.\n", "red")) + input("\n to close the program") + return False + else: + print(colored("\nInvalid input. Please try again.\n", "red")) + input("\nPress Enter to retry") + return False \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/term_extra.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/term_extra.py new file mode 100644 index 0000000..a77581b --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/settings/term_extra.py @@ -0,0 +1,81 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +import shutil + + +# ================================================== +# CREATE the application layout +# ================================================== +def print_header(title): + columns, rows = get_terminal_size() + print("-" * columns) + +def print_menu(options): + columns, rows = get_terminal_size() + half = len(options) // 2 + for i in range(half): + left_option = f"{i+1}. {options[i]}" + right_option = f"{i+1+half}. {options[i+half]}" if i + half < len(options) else '' + print(f"{left_option:<{columns//2}}{right_option}") + +def print_footer(footer_text): + columns, _ = get_terminal_size() + print("\n") + lines = footer_text.split('\n') + for line in lines: + print(line.ljust(columns)) + print("\n") + + +# ================================================== +# CLEAR the screen and present the main menu +# ================================================== +def clear_screen(): + os.system('clear') + +def print_ascii_art(): + ascii_art = """ + ____ _ __ __ _ _ ____ _ _ _ + / ___(_)___ ___ ___ | \/ | ___ _ __ __ _| | _(_) / ___| | | | | | +| | | / __|/ __/ _ \ | |\/| |/ _ \ '__/ _` | |/ / | | | | | | | | | +| |___| \__ \ (_| (_) | | | | | __/ | | (_| | <| | | |___| |__| |_| | + \____|_|___/\___\___/ |_| |_|\___|_| \__,_|_|\_\_| \____|_____\___/ +""" + print(ascii_art) + +def get_terminal_size(): + columns, rows = shutil.get_terminal_size() + return columns, rows \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/setup.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/setup.py new file mode 100644 index 0000000..133e3e8 --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/setup.py @@ -0,0 +1,61 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +from setuptools import setup, find_packages + + +# ================================================== +# SETUP the environment with needed libraries +# ================================================== +setup( + name='Cisco Meraki CLU', + version='1.3', + packages=find_packages(), + install_requires=[ + 'tabulate', + 'pathlib', + 'datetime', + 'termcolor', + 'requests', + 'pysqlcipher3', + 'rich', + 'setuptools' + ], + include_package_data=True, + entry_points={ + 'console_scripts': [ + 'ciscomerakiclu = ciscomerakiclu.main:main' + ] + } +) \ No newline at end of file diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/utilities/__init__.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/utilities/submenu.py b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/utilities/submenu.py new file mode 100644 index 0000000..11e666a --- /dev/null +++ b/Source code/LINUX_MACOS/opt/akamura/ciscomerakiclu/utilities/submenu.py @@ -0,0 +1,168 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +from pathlib import Path +from datetime import datetime +from termcolor import colored + + +# ================================================== +# IMPORT custom modules +# ================================================== +from modules.meraki import meraki_api +from modules.meraki import meraki_ms_mr +from modules.meraki import meraki_mx +from settings import term_extra + + +# ================================================== +# VISUALIZE submenus for Appliance, Switches and APs +# ================================================== +def select_organization(api_key): + selected_org = meraki_api.select_organization(api_key) + return selected_org + +def submenu_sw_and_ap(api_key): + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + header = "" + options = ["Select an Organization", "Return to Main Menu"] + term_extra.print_header(header) + term_extra.print_menu(options) + choice = input(colored("\nChoose a menu option [1-2]: ", "cyan")) + + if choice == '1': + selected_org = select_organization(api_key) + if selected_org: + term_extra.clear_screen() + term_extra.print_ascii_art() + term_extra.print_header(header) + print(colored(f"\nYou selected {selected_org['name']}.\n", "green")) + select_network(api_key, selected_org['id']) + elif choice == '2': + break + +def submenu_mx(api_key): + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + header = "" + options = ["Select an Organization", "Return to Main Menu"] + term_extra.print_header(header) + term_extra.print_menu(options) + choice = input(colored("\nChoose a menu option [1-2]: ", "cyan")) + + if choice == '1': + selected_org = select_organization(api_key) + if selected_org: + term_extra.clear_screen() + term_extra.print_ascii_art() + term_extra.print_header(header) + print(colored(f"\nYou selected {selected_org['name']}.\n", "green")) + meraki_mx.select_mx_network(api_key, selected_org['id']) + elif choice == '2': + break + + +# ================================================== +# DEFINE how to process data inside Networks +# ================================================== +def select_network(api_key, organization_id): + selected_network = meraki_api.select_network(api_key, organization_id) + if selected_network: + network_name = selected_network['name'] + network_id = selected_network['id'] + + downloads_path = str(Path.home() / "Downloads") + current_date = datetime.now().strftime("%Y-%m-%d") + meraki_dir = os.path.join(downloads_path, f"Cisco-Meraki-CLU-Export-{current_date}") + os.makedirs(meraki_dir, exist_ok=True) + + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + header = "Network Menu" + options = [ + "Get Switches", + "Get Access Points", + "Get Switch Ports", + "Get Devices Statuses", + "Download Switches CSV", + "Download Access Points CSV", + "Download Devices Statuses CSV (under dev)", + "Return to Main Menu" + ] + term_extra.print_header(header) + term_extra.print_menu(options) + + columns, _ = term_extra.get_terminal_size() + print("-" * columns) + + choice = input(colored("\nChoose a menu option [1-8]: ", "cyan")) + + if choice == '1': + meraki_ms_mr.display_devices(api_key, network_id, 'switches') + elif choice == '2': + meraki_ms_mr.display_devices(api_key, network_id, 'access_points') + elif choice == '3': + serial_number = input("\nEnter the switch serial number: ") + if serial_number: + print(f"Fetching switch ports for serial: {serial_number}") + meraki_ms_mr.display_switch_ports(api_key, serial_number) + else: + print("[red]Invalid input. Please enter a valid serial number.[/red]") + elif choice == '4': + meraki_ms_mr.display_organization_devices_statuses(api_key, organization_id, network_id) + elif choice == '5': + switches = meraki_api.get_meraki_switches(api_key, network_id) + if switches: + meraki_api.export_devices_to_csv(switches, network_name, 'switches', meraki_dir) + else: + print("No switches to download.") + choice = input(colored("\nPress Enter to return to the precedent menu...", "green")) + + elif choice == '6': + access_points = meraki_api.get_meraki_access_points(api_key, network_id) + if access_points: + meraki_api.export_devices_to_csv(access_points, network_name, 'access_points', meraki_dir) + else: + print("No access points to download.") + choice = input(colored("\nPress Enter to return to the precedent menu...", "green")) + + elif choice == '8': + break + else: + print("[red]No network selected or invalid organization ID.[/red]") \ No newline at end of file diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/api/__init__.py b/Source code/WINDOWS/akamura/ciscomerakiclu/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/api/meraki_api_manager.py b/Source code/WINDOWS/akamura/ciscomerakiclu/api/meraki_api_manager.py new file mode 100644 index 0000000..13506a8 --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/api/meraki_api_manager.py @@ -0,0 +1,76 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +import sqlite3 +from cryptography.fernet import Fernet +from base64 import urlsafe_b64encode +from termcolor import colored + +# Utility function to generate a Fernet key from a password +def generate_fernet_key(password): + # This is a simple way to ensure the key size fits Fernet's requirements + return Fernet(urlsafe_b64encode(password.encode('utf-8').ljust(32)[:32])) + +db_path = os.path.join(os.path.dirname(__file__), '..', 'db', 'cisco_meraki_clu_db.db') + +def save_api_key(api_key, fernet): + # Note: fernet is now directly used, assuming it's correctly passed as a Fernet object + encrypted_api_key = fernet.encrypt(api_key.encode('utf-8')) + + try: + conn = sqlite3.connect(db_path) + conn.execute("INSERT OR REPLACE INTO sensitive_data (id, data) VALUES (1, ?)", (encrypted_api_key,)) + conn.commit() + print("API key saved successfully.") + except Exception as e: + print(f"An error occurred: {e}") + finally: + conn.close() + +def get_api_key(fernet): + # Note: fernet is now directly used, assuming it's correctly passed as a Fernet object + + try: + conn = sqlite3.connect(db_path) + cursor = conn.execute("SELECT data FROM sensitive_data WHERE id = 1") + encrypted_api_key = cursor.fetchone() + api_key = fernet.decrypt(encrypted_api_key[0]) if encrypted_api_key else None + return api_key.decode('utf-8') if api_key else None + except Exception as e: + print(colored("An error occurred while accessing the database: ", "red") + str(e)) + return None + finally: + if conn: + conn.close() \ No newline at end of file diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/icons/cisco-meraki-icon.ico b/Source code/WINDOWS/akamura/ciscomerakiclu/icons/cisco-meraki-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..be7083004345e3052133b37c3546458fc4b1baca GIT binary patch literal 35462 zcmd4(c|4T;+Xjwb82i3c7_vsT?9xz@%2vseZ7gL;_B~^ak%*#5$}UM|Eqj000{BFZB-%AP)E^0)PPc z`tC(@V|JE9EZ|G_^XJZ71^`;{B`v_j0DjmE|FQ>wUwr<|sVm{xOU+mNq&q`oLeD)g zqrLx_L7}UohaM$&LCOe_s(E1hT)5l+As>?9sh;V78!D3Q|NacIds^05yOkTc5qqHG zqJOKp4>|q%=|gGP0;Yto^}K*F->J8aF_ZgKE7*zkwu!f|19j}(XUigC3|}Ye%#rig ztmk&$%2&Of98hN$Vq=KKIe$IY6tV#PeyQ>(-t5n-uLfTsEzT-JYi|&|K+pq71L@tIG)nA^i{E4b zNH&0?90JISfS!u~F{(jSJlz5$_CJt%Dg1yF4T{1JkVQYsFt)31^0Y)V15=DB3Y{s5 z9sJTtop8W?bVu@umrDSf22DLTfMEclIjY7EexGym$RwwlBI(fQ>Ni4no&ZfyAeRv} z1#PJV-$RH^SKYnCJYW;Oa$JT7n-9KA9Hz+xJ4eE2yV;+)u4Aw}b$MDfexc z--p<)O`Uqt3^c&$N#}w3tjqachra5M-Z?1r0RZqt%!{XU3Ej?LnbZv#xlp$pYOEgt z(fnNe_A~D|xo=taE!yMD2t9SZldC~(cP+tJIk)SY;)0i>7(Z8U3NL>Rv;bhSs+pR` zl~vKqwKDTAJNVLPeJv&#Z0?>RKh=xnxoT8eU_Z_oU^G z?s{hJYOhb!QI6zoysRLGZk0T{gIg|c8k>8ocRnMC7Qg%Y65V`xGt0(QZEI9cnD zT!ez)HF7~K$Cu62xhxV7-4S}d)l}BIK7$|Vxmv$c<`;hg*$_4YKe*H~ZUUG%jDb%b z7VY#g$Hfr*E@LyB-7Fi0BhT=5X(5zx=LE0p=ZA_2{U*`S-X(W!s2fz4*RlUslb%5Hn{wDoC8#nCsr z4-Yt6Ca0~wPGX;o-FJCksesVADT-yc?;KHifARiCEfQl_^R!~c5w(1v;6HPMd|trs z+bZBNPWJK@yz>xOPMH00D_uYM7~5^Pb7zYA5H&VLDZ_icfi^_7=-fWFkb1rOO6bx$ zyn6{7G=hBgtKVq&1Yq2`Um58yUCi2c&KY#X%PQ9Pat7jz!5OXX62vV2rYO8)oEYuo z8W||eku2-~{ax5=#(^3rdm z=e152{9*+%KIqbD>rH+-@As1JKO6?4vb9u^=(q%(?H{7ZyB7lc0w|XQ&?5hZuItWW5$iM3(hE4F&jvLx4JZFMYdyw4Lk>oV4$}UaADhJ`DpVN zSSF(;%?BplC}C5=e&yI66^J0#{yKfVRR%}|PcM+c*~pdC@*?hXw-+;GZhwPBn~T@V zv1S2OWBePB1WXRsg&;CxRZC|~oOuCSkZ@oAl*c~CdD-mpNS0y)SdcI~`%X@t1jQ>8 zcR`~-OgFEq;8sBwQXh+IWf@`+?Hl^3>nq!Dm<bMM*EPOBXdx7H_(w$2k z7QpX!cx=;*hU%krj&0-!)+XnDvygGO@61c9O!+v${!*Ww#Xq&;YEet@+6LD(NeQ;~lYtuM=EPjDPgtwIRRLO|R919}ZGae$zA z=OyXHd_6t)_D*&jPEH1(<^x^i;o9U1mxO{t^FC4boy9tpA*AXO5!i6x|DS6pND!qf zHF<$Q&H&B+Z!j>pi3`&az^y(TWd>oyrooIk9l-wt?=9r|vLhnh4{Z@t*@MoD%WI#2 zkjrCl{MSv6<1&tq>5{W=M_pMd=fH8=YF*>uW5@#j00YU6=A<&%fBJ{G@>-BYE2m4N z(w~8YU^)Ldzih_ji5X!Y;>wEo^qM}8T*dO%9Q47z&`HI0PYHq-1VM-Z=@{wQ$}I>s z>Bcrtsd@xwv!h~%`^`kVJ*>Q>&&cDd|8XrFaiU}C6liGu!!G~FrG_sNeva=}it(bj z?^5&TAMUj8{2LdD3@cXH!94EL{9nh2_CtOL+kajvfC(uWVhWsdi&0(V1FCE4lF=6P0!9zn zXJr6Mo+yx~{-CHf`R`C7DwPNz_Ih2}p0;h60|^>xIOWS!=BH-g0H@h3{Gdo_72ado zBcS39-h;)x?<7#ef)wENMyC7y&e8I`h@(L(A*7|v+GL9WqdGBvQT=F%L&HC;T4acZ zE#>RdXKkZr_(+KA+GMB{2-}Gv$z1QIWPO8yPcWL7R-0yo{l74!`*#H68%ym7KlPIy znh>&DrfMGe(DtDQ?p`_lcUtL-ms^wYdwvGOFEf?srSi(VfI39?<$hnf=9_PgGX{|FHxyf#sq@0DiM zZl#*d(U$OS{Mi>>G2H5rMTkL}xwjG#2u)X3Yu!6sjGr%LXF|f_8(w8_4F8gkhWv(G z!R3S~37H@hO<&#U8?JP+tY#M zeO53K;E1U>wr zObRGca`Q|-n>oePRJ{8UNOlNBNtPNC=!WCy^STu;l*tfbkFtQ$20YzYpQxwY?ei^l zTWSrxhKIRG2#z32Wzo_hD>4%#uSYTQbP?D7Y16sKHyx7hggo@4?E@NzYt$bzQ`38p zGcHB!1L(g$iRN#ZQY|0E1bZ4L{^boL@XNFGpUbC%NfS&AC#04N1H(cRKw^*?xhAMH zTI+RSHq{2^88bnIQKlZuj1U2Gj#h*yL({L+EPSD3e;F$)EX)3poz{6mP=se8n1>l~ zj9?->E}7UHQ10ZTFd5 z9-u4ErF9lF!+I@b=4u3^coB&T??PN~9zjW$7iE}{Ep%;O(KmL26v3a<(>H7 zZ#=|H;;~JlCK9TYg8VDd{Q(Jh$YOwfv|P@sKw6a_J_jJrUrQ)>3yK4GUPSz#9MZ3a z>-~|rnG6nAN{hNVPPHi53L&r#f12D;hSEd%3GfidGBrQ&@gkpym z#fb@M86r2QY5ZuwuelNTU5d_VRpJhCOy%}M7G98$9uva$4CH-Jo8ZNV%#u&|9`y zAo`Fr@Q?qUb9dUiv{1dmN>bA5T31dy6>AfCo2W~=NC}IF8 zZa0cP>oVZ##`fg2^cEj7A;+Ji@VxhzYcY2U*I1E)Tg{B1(wqBl^10+h_~zs#4Khf! zqyR~V5T^KLje1<~Mq11_$drT&G`+FJZgb=mft%ipz$+%t73G5yt>(N4 zNG$o=%Q;m8LAT~I_ys}+_GUG{_xxAm*_GD<2O(UDGpBe-x8qsP!0ib}?S-Yf??)rN z8=9UL&{?U{2$g`@M)bFKHEw)npYo%Y{DKvkRcDUl3>>wBy{py}{H_9a2YnYm+5R71 zzJ`Xgv{MDs@wpH_(gvlTH7&=@h!Ke%TWO}53|Q9{73BV{(>zo^vckp`%A|1Z-$p|5 zME{S>jLYNk&AMk@9N!@diqvphU6r55lAxTudC-0%=<&KqE!+LT@U0D%+u)*k3 z0;^9ZzEK-?)7fwF+=0A4_njyeEUFWg7>hDz^+P57$jfeY40h>qJ6pW;8|!2+h{@zm}TJ}M3_f@JJz z+iB{&fh~h);CF;yH0D#IM>9@ivC*J?;ti49^iW~_1>$Q_*>&fq5vGtb{M)Y^F zc+&9B=}T79L`9giYj;w*nIK7Nb0vMR4|SvL(8c1VO3h_>W<-3KdQ8`ulQVwVlyEk# zq~plyW3Vl9bOUMMRn3{L`s>MIZnT3>>vyv&>LbEuGoQ*KagMZuwu`ZVS`P zbR;zfY$L_B&d!i5iWTjaACdTjx~QAGs}l-fIbHFnUXn*5x7-ycCA3+ZG5Lu9>Zh2?7g7svNMFnCpSfL!DO!_d-L2ooa5v- z;WUa|^jO6`-IYadl2U9re&^eDDPJT=C4(oP-9IG@6hWeQKb>VIXFq?A7tGesL#%pC z=lJQ@1UCdp*Rnm}B5n4%Rz?g#@~jUBcin(M_s~-s;(_m?UM~zq^F`ro_@j!_p34%= ztZ^QL*SSb5{UUNO8aa7lA9vLCA21#7y5u|)#fRZPCao?{dMVO{(j~4%s6F3*k3x%vmAm5y(ye}(<%!XThK}e1=2NfT>MAKRDAsE?$Ys= z=CW@d=n@{1@2TU;Sj~mhAtu-+J=VJg|J?O$2&6N?I2E7XJbe;LtjZ*sR z0~R*bk56Zmo+T706CLRoijnuo&@iR4y&mvxCPFM=NiO*+LKJ&Pq`{zr86`=zA6xEQ z&6lt7-XCdt9FrkXe=GL!Fxq39bV}j^2n!qzbVh0Wc@&&*@+#*~b>nO&3@WXyi;qLL z1i7&o27AqCr1N3468_E_2cPp)w|T)`GPso_0X9eY5wmt4eiuPAKFdqKc#_NxGLk?R zjo{a(6*JG=&R(gO$H^vO1RQX1u-LOTUdbusoZ9uA_Mk+3I7+NdPAME(>TKBXF|we! zt*VK$c@?Wb{1MX!Kg99)P37;c0=9nJ_iSBX8LbSJ9{*D^%2HK+J*t=^QXwE3y z>LC^FEw_6I2_D}0G0kO$Ps}}PFy!|~iSMKGEE8=xPEx(9ROYU49yI%Dd&m@c+k5K@ zJ=~JReyzHUGm)VGQK>fRF0QXo->UrhQr$Aq-s9Q+zIDHbtr#@Ujib<>1l$c9|d2s_KQTI_aQpf?PWMU8+Ty_;bVIxVdtkc%unqs(tzzOQ*3y zZovq}##77uZxiZVEnc)-@S=H8zh1Dyj8Y&T>o3lc;V(_Xhef*wjih8*U>GN(8#kWf zOMcqWRng~(@7ej&M_c+X77wpAtoguoZ86v%ZTwNh&f&G6)mF?%@nw!2_+lo&CxA%r z2&2!_kNk1Ioe|@!bY%x9u7CvGA4{Ys}RZCNa?DXDKgWr zp|J2<3zF`%3c}WIpr@D`zJFl?&6&BeSjh{#0hKNTNPNYE42s*j5O~&3Q58Jyz-?Cw zKeK}@uMzg&|4xGuef?i{9rihJ(vGPQt?0=;V0;oqoNr^PAWv9kwp=b4;8bHm?#RjJ zKA5`z3TGL+?zaJJmyoo7+c+pu)(o_AnnS#XTLZ9&EQ8jsol<}R0>)Fq>=#F%& zFRlubHY1dTe}lD(85Vp-AJt`X*7eihy8y$#e!&e%GysM&mnN@w)RTpL+_48k%s#QbMnKh4$DK8&iEZD&oC*z-i&^J=G56`>J%O&iQp8N4t~z z6ym{p38iaIleF+_&^4F-tJUX!4}uxB zf&2<&>Nla5p$YLg&GHy)b&upcL*-%}rg{T|e>|xhKH;$jdim z%6fanT^(Eh4F?&VtOnM6q_}U8Ry@F~BTnt}NvNDUO@5Ay4_MG3cRVz;#B?B7?`L-z z+-4JA=6j+~eU73Ont)mr1YxAk>?iRDOQnChA%)@W4DRSz*|~x$%hYi^xe`R*Zh!!f9_|5YT0l z%qk~WZZb$Y^HSB;$2EQ)gkTjXMZ1x*N>uNC6B`s+flH4Ka?KSV*BiN*sc_P@9X-|8 zOA|wNLp29_N(wpDv4>=n8~9m$Dk&-)$9YCV>9z$lLU$=EQdpCYcBLByOdOIim*ZKK zA-;ZA@#VS{Us{>u6Z2xAuSHRIiVoVap^6j_(%YG%NX{)tRN<>OZW{*`f!zb%vUA_+ z{qGpEAO#mURK-a;BcVohN?#;+!4iXAk$BPuAdu(}8i9gtR5>+)-&G#bh1G3Nrnk-N zYfO@~bSCdWouYIn|d_1=Vsek{K(1SJXBP@5%lbW9N+{-!*>0ZT}njiXb7L zKe%w$WWTFALGeBVz8@LnAqI!jljP`c_sR^Z2MySLDN<-_?UVFD4c ztjG+SQRO2+OcUMFJJ>~ubPoy~^KwwD?Ss1OiR#(75Iw4et&&V=Vxw8bzx+H7y0DWu zF!$IgqzxCs&L^-YHbt%1-QbB2!?i1}6A-x7X1aM(E?#RvdA=b_U;pPOy;8YIc5L)G z^8DvP<2&4oM_+zR@j_3XQMjzBlxRFUSXM7M&J7&K+0Z_l_CHV8hvi zTRfqh6ybaICPTK3>kf;DT0Tb;;-+j?O)@8n7a^EdHiStzY@UtgRJciw9FE#l9&Gs_ zO&qv5=x`S}=a3mN&Bn-0qV1~rPzMa32J>ZA!`cT4q9ZST%4RCP?n;<4g^@qkq8i4F z(Cf>hDN5E16U{rar!>ApjwbeWUdcV6^v@Q1M3i%D*>D~!Pnet&v`(QvvOJf<>bIbO ztT!*)Hc(;}BHm`O(6`8pe8F)y;k1LC@3XYfBGnr+jTw{+*Q`FvOh6a4ylVbKIC&%V}_{=sXrCg6agR>{SXUa!P-R7z8KhWmyjC=k$$GXZKsE&P+ zlQ>PU&lOkCwDSk{e727W}S<2cWXcM$YOa(p5?dYSO`@=4cSOTI1di|4!E-=9|Ph9e( z>XT31AN-%Y?AG}?euY?F@oAf0vP*{&nO>_+eyl z`vD>41zV?A5HluEfg8AMqNbExKv7X1xOSA*=fx!Yv=uL4X9y`}4L+x;{EQ1J{v9WP zIL+7-(>D`gv(t-P9J`Q3lf&7Pph@@kL?$?=L)~8{R>rh-1@Eq=jBmaCtO_&*uX&$a zHHBVQVocWlV#naG zMJILUtkP33h^GWr$+Df-s%^M)8;;eR95eNKT=2hMOd_R-#>Qw;@f_ zE(=t52sshxz-UkBLi}`2HBN(0HIQult)|-)Fkpm6(Eo;eYA$}>v2%W4*-3}?N$1A& z34O(6ueIqsyG|ymq>=O<=IpjA!Se>bfb@9M=6t%X`PKp;f^o(CZwB`c;7Bk+-0)*) zsyKM+T(Q6_Ssg+ua9vDWay)_lxwhmm?*E;SG#9C_oV=Fhhb(xln(AA1CRzG7i#ssl zM!ZUTNWOf;Q`TKX!v_>6Y8m@eh4i)8dqRroG0_UO_W~?TtY)X7= z5Vz_|R8*Z#YBN?iMV?(+e4JK1-S)g2bx@#SDh!)LP>aX}R zjBoicjF*NWPzX>t8iF&>s;=?R>*ZN1@r}N>P>WG_GnAvs_$|%_X`lp2^-nxI;^tmk z5twx>X|;@N-*!|UC$G z*C6s;unDuD8q$1@4`Y1?Cl3;rR>5pUNb^=^|FXsFrtJIbXZrPe4s^w!bxa5X9XlQc z4$<-r;`ug?u(WduQXxK!Rv;eA`_vs1x9?Z7Oi%p$7Sm{x;;fr)RX$|O;v-YIkWp?< z5IwRNxwStWpLSHlJUIjV8f3H?*0@HP@6yktCk}*0nOE?m0RgTqPmZ{yP7N#PTvmu= z5m!@wQ@CwLtm#rD+W9n^qo-I3`|TOPs-#5Wy_d)G6Op{rSzSl}X!g?$Wm7vV&^L`e zyrR%%lleA%{rdGsPhxL_J;~pg2S55c6uqgPqV1Eva+**SdtQwFvD!lyp=>)sU?{Tb zmOqD6!)vUt|NP~>p_WYa)OX&Dl_U(GywztpP{XgRP%I{8u?Lhk1&cQRxLFw`^-jUe z5^J374^!rGk)w{Cy$?GZgh*y6)yhLGmpm`xQW*33*dJ4CaP_cw-*K#z$j@eBe`&rd z`wxS|Yu0if(H6ybS&{s8TcYT3u&KEEYw24w>RS~Bygr#YwKRkc@Jy!fUW=Q%AXFZU zU#w1F(Ixkxr;Mz4Q9Nj!Hqy_2`NKd#q=?jP9-38!x(18QHxM1@)E%`YDM4 z+s^rpIi)GmT-16ErLT%oBNL&BTg`lqEPCI5H_j#T-YZamjvw6UV{Rd!E53R?RSL{@ z_Q9`XZ%5X*&BS=Oll|OQ&ZZBHI6++pzO3X1g`t!N58Yq$)#2f$C8Ek+)heQ z(WasuTW&oCu6VZy>|8A5#h8IQvd_FW`7x!5DpGR~!on!BbaLwceLjDYjBj%3bx~|J z7*6w+K*H%*7Nj0frN^C{0|OUz6<|BlyIfZ`qPHugMj*@L_mV&!>Q*@-0{;tE*&|cDC3U#!ab5WvUc_T=#96nDv>~fsFhk= zz6tx6nEmXn@|noC5B)-Hq~QJQ>BcoH*aNpu-6-~IExLhzUlIGx`F_{>Q00npa`a+U zo}m7|zRqOs$^8^AlIuinj&Vf4j0v}#H!tGg$q#hJ!N&mTM>9a4i{we|)8)&89TI_J zWOnM;K%8iJbhML)8s3eQwD$H!TA5pG7R|m#3$+fsQoa%&4g20S7?T8bK(fdfFLP4Z z29~|~NvCxNyC2Tjbt+2|9l;?Qm##PQoD9zRy(~y)B@ecaxBr~|JS5HrVT7VS!T{9v zC%5leG&5*;yH8YnJ_`u0$ALIRs%G>yW%#f~$vBxZmj&~S>T$VSmbG!9m-Vm zE`6x!p>wRTNlWm?Z#8ivDX)}py{axB_V^4af(>|M6Kk-|BuFZ!NwU;8KYud`{mr9> z_IsW$mTi6;d5puQR+w<|Q+Tbi*jHT`_{h-CdiT789MK58BQ)0DPj5oeydy~vlc|~b z{&_4d8ZPJYPUr2%5QO_Nuzm>%cv#?e-HGN!r%Bh3WHTLhlS`obOPs zi6`LX({uVMo#D2L6WxX z{Umnw6#SqOT(#DGx&SgI8p(K~Qzb`#zhZkjkCkjgIlE}<>)sb6dl#gJFCvPyiI z>Q{*PZ@asf&r}qG{|74xoAPaa{OR_aYWQET1Lp@$9fxUxLSGKCHr&sH;I=`FU7x<| zvG#3>0e}ULxPAAaD6i(lt->ST-N0ES!eUIs(!hzQ5FN=dHA^m%)wSLC(ERT<$MIHF|5Y){&q9c5dfb_(KC5+HdQpWb}YPBM@%byz8h*3hIJv<;j1 zM+6Ee5~KQU#F*$LNDeR{s})ibZp-(4hDKE-CZFx;oI{Q}lNUXj?wc4rg<*MD&f65Un*0>lf3*?^zg zP?UXdef&4*b%xg>bDDW@jIv5g8^p93AL7FS_m-(mABJP%#OHGsOqd&i9k$~1d51C@ zg*OVrPRby+qXV1R_3KGr?3gf8ps)d#xQ@tZji@m1`aG(@^22FAXdp=F-ridc^w_)P z1UH3H`)L17`n-J}v9jwZi%D>hu`jY>HtphcQXDTTl3)Za`M{g<21poAh5w&Hly&qxDU`7_5ZdTcRT4x&q^}` zrT!pQyA+(M^-0DYFdb)sDQCF73J{1n-SB;MOcy;~MT^&k=o6+7gA1jYSa$54dkZ$hdb{j_7!0O~wE^YWBm+)g``1VlVZp z0DV>24BgA|VYnbJrG00;sNxmaP%#Tr!>_WYI8BR-O`fXLl)LTxL&BBftru= z>c{DUg&i>xZ-EWUuZf#X;JiR!<>dOy1IE`HZj&^BaekKo$F#3y03>eSqZwQ(J`?@^ zjn5CCDCu*qk-N`&JkTC#sHb6IgZWu#$$7%bHGG5m%&)^MjyiT^#Nz$p>6I)f*I61u zYg`3nCb9i;Am}=*u%12xJ=APz_{RH1B7XuV37kKbgKU90jQ}GCbMIv+@-WInI4^w5 zW$m0Z9l)68ru_sf{NPZE%3szu1beW3QCq94@(u?SXUwj$kYEALX3&GO8QE{gnLw*o z;O8?{_;LVGR%G21-+Q`XKljILRcmlS_4sP}9lW7z`6tGCJhbjd77g@?AA>_nGwXvy z*)Io0=Ypm?oxat7e^W2R2#QzJLjR!q(K!l6fVL~HcQkKK zZd=}&+AQ%ShLFg@4`7+a9N^;r>B!wwaF{X`0%~u^lS^1(6$#>Qnvwoh7YNpt2h{o_ z*H(Ysc(!$zrI!q!)MuT-lK5WHT#)`LvF?@^?8GR&6Sv5Ppr^Rio((-}F05=4MNO z?)D*s9R6NyW^`n2O+DvwPLu2_xMI}NRq7_hNeoyye7abGrwgZt3fhhB6(zy%+qqRD z)bQD%^WR;-(&vn#T1TALW)?AeVoDeNbE!YNe)|2<^f3x%f>I;pCHSo;FBP9;6!09Z zk~{>h&+=Q)d{H7!n3o@N3qenPQ6{n%m9Gu&zM#?pVs-wp7&yH2uRKhI0X#o9?(!l; zGTa^3Y`@FY=Qe#MF#?1!78?r1e_6V|DY{lL22xU55Ua{UP7R;~_vPAq!qnN0(_HLp zlJg)Li)59}d~zFXIesc%XQsI%a`Pijhv>4@xOz^HP#$l$*jIke<$)OQ!L>MUlI!_n zg@4V`Be#CR?{iX`{HII4*V+xWZs?H>7HX7AEWrdF|5Sm9e}DPeB40GWKgS=!?})P3 zS0dU~d{Z3qIy$NPrWtXbY!1quESj*2a80_@@9VZ-e>@lL*m(=?X)E?GsM@^8|)*N=qx_-|un#jnTMv6d~ zeb`8@vwj>g&>k*J_p=ed)@H;vD@a0QMG%ZF3FvVWAG-oLOlB>N8U{1KJ9ufb;P{h;Wl30;!+Wr$H7>|6+61%{T;9%spOpTJ}L_9fEk zQ}y=bg()>Z1PxVUnxEbNVRj_c`}!hrZ*7y+#CNf<(~E&^Q{K54wxp&4u4x=cz5YEJ z-<)4lE67vm3uu0ROVD}FXZ53mdZ(E(Dujz#T*@iYUZ(>ZZESkUQ@3Wkz$Rm!KIQsL zYr^>jR*Ws5@gM86w-2fzS(v+ zm@m|@TdoO3r*Mq==>~|EeBq+3!jUZ7$+hQY-Ad8eYvTY;HmG1D=NVqB^$D|F5Tilg z9+*sc?IUo#UwnAc%3FKSzPq;==_P5=U1+%=<(W&1&db05R1B(F*33uQwD|!iN0JeH zdKnj5wf_NlssN(n+#zsS_(6-tdB}aKZVQy{GrHhjrdr7KJ@nM<-iabDI@mNKcbk|% zb~o@na{lSxwdClnRmy~xe*Vrh^pxpQ&y?GC^iYdIo2L5?ot!`36sQm^0XJ|I`q!qx zW(bMsf9X_V0Eb=hrSh&dfu@&)C{&~0Eno%XwUwIrC5tAA8JR}Zi1T~}=J59N!O%lC zl&P$wXmEwb&^>;Q1(}g!AD$3`xG|m?^(DTQ7j;$Rrf42#I>$5z>P{>FPHti04gaE< z8}?1Xr$A_2C`IHkJDtE8Gn#3#e|O#yO6K(V_I;06>vbtbKWasJgPDwahD|Wa^u8g8 zIG%CXT7Bv$3$iUmM=VZ>#S{Sd1JEJdB$^E;&Y$()CAVblOH$g-;m6p{nM?g#D8#-n z;2}ZEtfOrM)GHei_jvGbVJDT4+Z26xK3A;r9RE_;M zd3dQ%){Oi15``ptUyE|mqP}tP_2H7Y?+ULyq;!Pa!@p$E?k1<=`Y-`r5VPs~&+uwP zR?W=*G1hcPuLl>yCZ@i9JO zOH%V5SP^>8Maud$uc_-1SK`mWK=PX2&&V}>FV%J8z|Je5Kjk*v%lUH4*J(S!dq5gv z`fBRstnDd3+Q9Lba(Xs5GG`QTgR6gUV+8T8mesfDpP#%O!$fL9}|hW3@%$Ec`yf`)0vFUsILa2H%xe9xePjsCsD@ zyD3KENwAq8Z7T1T<77xBoxKyw0G4}>dn zt2XRL`wp8qp2ngs^j_lH1JoK-v|;Q{< zXP|bh{Km0&pNeXP`wmPAJw;uh#1zL7PP+0iBw0KG8F-scXMx?(q@vqoog>uN_%pX>c${?+ir?)5BBYH$|a~ zDd+E;(Z~X5g6W~oYV>)0_NNINsc{0;Gp#bIUHzJ*mW@$c`4!AAP5u5HA&cfbI?od* ztF;RtfTL;EWxp~RV`$@kz~#>{#OeU4P2XI-24e%~qN$%RcE@$Pt9;ZR5;jiaD|U^ZvxxN+nv{;=Ny0-E0}OkjF3m;i7evZRRA=5@|% zJIl6v4Q7fQsDX*!fyh#NLpoZog68EpLY5UWVErDCS|i~}UZ=;-^2G*-OOKLuFs-=7 zmE{3&pXB-MY~ZmF4TH`&m74(m^NP2BXhu~`>HVqtMOEs8T2bTV4$Xu6ucA3NN@s#p zzUevzjt9>C9Cqa8P+A5HSEpMSCe3|wk4}Euj}31gUTbrzcS|3fYQ668{e?%%@>qW$ zcWY=vlW^^483X*CDX&EHPACjxH4Rqb7D2Pim97=chLo#b;FCFPJ&X$ge|onp?Axk? z-WkU7q1Q>zQzJ$5#0HnJ6DB2^{q=j4N$JUTGrD&TXK$NN4&L-TkHl5XfNkl)c8xRI zSMSPLB`v)d6&Pj z5iOvkG-;okYWc%D!MrA2AqbvclCp86rFOPyMkwdeKb%TXZD=@q@znB4UNqYb3%K<= z49~8NW}4SA))gfMwKTu*G%(w=;<3(OJ=g5benC}GM=2%F9ayR-fIYai(_po@s`cX) zlMlle{rI-Bnd17>?^jYkbgm#BNK2Gx%=Qrn76h@F=ED)<(|7vJb`9;rYs_Sx+slUG zs}1`@058hiQvJcQJp966`y38;RX)b~|I#WjE_uTS^pRg2Ob$tE>prR(E@jv%o!O~> z@IKAEBt<%EX~j5>8$c`~_N{u{_z{4Mo?-Q7_usn!3kbpV(dy_pJprW|9<7sfD>_M! zlelU>O*cm>s)g1b$)oeth*M+I`dJH;6|1Xshk_-9@h|baD$`$}N)^TR(mhKU29HOY zbno(NavazZ(kh26n-_`Mg3-%5=IxExpquX-NFMDCtXcaU7w+2fjRuD6zH z&X1ZIWYQcD26<1XTtC4&KZ*%%Sy8m`W8gsmZDk^o7Ywqj;(Y3xN`o`->jtf_RLm|p z;I#Vu9C6Qm$k*V8J)K1@~NybH-f1EU6V zx_(||_imlqUl9s)Ge3I?=W{@o$o5c{K0^gMU+a|CjAVG>ffv9{6HoPs|)bgv?R8D3`?_lOb4~ExN*(M?dyQ{sLN%LWo%F$b^ zmL36Fl+lNm#$H)|?25?rqmhy z)|XdAGyu;@qps>ag|||3DmK@i%d@>6Cv+C2*3K}9HE|)zj)_flqZ&t&_Xa+^bgD(R zAITLio_sg&OKzN>W1P;-f}l=U;3$&MEd^YDGt>WMlM{~zWBjy`nXcS zpc;OA*|XsTZd;-$tU~%3k*lFDkr{QNeF8bSdgA8lQoUYvrmR+X9Q4)oneS^`-@bu~ zrB5^J>|!?xyDnS#pyQxycYjqFTnV2mb9dI*I$|~8RMi;iRl|5>WC(kqH(8cP!=|A3-ex_ZgsG<4+Vo>zm7kifj$P5d;nTyVp)$UGbLcK2%&fxoA-K~<4K56fN-jgv1XieM^eH?b_`0{*W zYwIE5S1hrK(2Td1_v;_L@oz)I+T2^fjnM77l=U2afwj-JTZ<9hzx2a4ri%G|DbWfI z`i1%lB@q>vKGv|(z;{OV_e{n&*@$SNqk=kLVYhcyYR>S@kMv3s=>xs^BZ0^M`)1hH z=d27DD#r#4OCp>G9EcC?G|Sn{nai72m?SL}mIzvNYxP?lg9Lmd)UG4#mXu1IwOZxd z{v%fIB!?A|v=ZY!QxE;|&>63_;%GrAc`cl=j{9{)1#FDBUki4hOLBp91Na$l* zflH!anGNz+AF$lIP{~+SoIeLx{EpDyTPo>n%?=O`yMTtfMKKJymu{)y{jh!^vyIr0 zaXa45PW6hc{232k5-?2;-5;UhxVkXXkk;tA4=0xh!7j@O;bJCTnAn%&= zEZb@)@OO*+5@B3%H}C|u%SGPkB7cS;5Z4qEV<+66h?OxxdCDoPqCOGeH0Zm8jY3(v zs3AS!orgjqKD^vn6dU>EdxOVaw(Ll95m@#{=O16=C?G(@PdYr0z5LlI`CoohQNCQZ z7ot-%t3i*o{}E9ZM%%QyVe);`-Js68t#qbSl2xm$v2)ro=YUfLu`^&<8CKn%ESp$u zeIxvmp>|@pbbY@uH3nzF7<^2P!Sv=b?6E=>4%H8OH0MH%*YdB;1s>CAm3MfFaL_iy zGAobj4mcf7dX9GNOxbvz`-B*%U)o}PWrR#l3 zbXS7+^#PNvHPpvSaI^I+KN_y`P@tKvr97`VL}N_8dx#nM*1ypir?wgYY4L5>Wx8Nj ztvCL|YX=VMP5G5<03O3q7t^f32yXHLufYtNTZIjQbO(dKx~DEzj~%;?O&)vVgNLqY5m#o5@u?l)#GKsq*4jA7c=n|~@BM9E zSjiQ&{q?CMV)u8*l!GjIkLOEHvL@b&FP5OX=tmn) zm#up#r5NR3{PtVifc1xn2}gK5MDnr2Jm(?Qd^)HGY}yM}=^!%_(wPv!i(oE%?bR6+ z{IvyFzVT~{lO& zsK}nZ>@AeN5GDJbF@wlX2$f|-rIHXuk(sHOELl=yok6lQ_Q9BWF86(ZzT5M8e$VlH z`l~sPnt8vkb3d=w`8ow>F2xJGV~#l~1yx70ERXuMbp_twSkb0Q%G&tP=O3affm=rH z%8JuAe>+lukI{3=1#wD0@fh>xSUZc#>-u$VPxorcI-e%J4~K-eHh!9wdGxq{W%T@g z^(D*KpsVgZLi5+znLe;6W2ZcB_NhEe@gVE{Dp6Ig{$msfz^NqpHxs~H)7~KJ4R#&XTy~cd53J1cpg+7QI;}Nt4I_RF^ z4Y%X~yk{b(B0}P#`ncJg;DMj?$kLgkreC%0KiX8x;%xEl*l-|}yH3^1^b2V@6cs?O z>}NNR-vo|x?j>IOe(Ln0yEjyRYofI$HudQ%Zv}t=Uyr5h=}h=C{eFAFb>+lZ1s3~u zr*p+{OMAZ&@Ls){=ed1D|IE9jC)|qXG9S*o&w%$SB`&MDm>bzH{Sw>N1Y<+XrLVi8 zTDYQyuV%hfyGL={QwDSBZ(@eFH3g^eSp8FxL7Up}MeheQq>s)sV=C`E|ySX6>U;(Xl*tZ{| z0*65x|IR0Xq-)%fbQ;owjXo+HhG9%f$@ziFp}XkgCLZFi9LYttr~ZT@2M|ZB;t*_1 zMUW@f#RP_*y}|qDj_5x(JW{eT)NZLbMp$Fgukw49^6oqLd-*8SVfh>g=tPV~&SxI3 ztl+Y69s1bxA!?RjNF{&$#(B ztlM{j0QY3jY{V#AbssgD%FF(lPw20HXq{8-wJgpM#BlxCkK58@uDq267ko2sNmYb{ z{q~PPI=Z(=-slgC)E*scmZolikXS@UX#91~2+z6%Xqa8B` zs7yaj!DzZS)!fRtHIvPPn`~;rX9I+~|4|gJ{Ni=Qj5_r6Qh-o?!>o`8;(#R`Q$gSN z)C;1C?8E&l7m8WEj(xr!3mt~z^vx0Q*>!a~%73c-Bm*UGf zu30?&{y5#|VovwRYdeVCf$`IT@H#I@sY(I?%ksnu)*AX2F zY`d3{d!)4C)iGzhzm0S@tzRVZD_rk~rta*kZ^PC;PP;a4thfss`sPVI)?rjN8|2^besy}*H z@L8YGWE-#gr*b`s;XV3Vk^T7ea8*#Fm%0-@!h}WlC+0vv@kbR&hUsFoBOx=gF}_oUSl=kuw8d+2W5qs|K=$&r zy*Y1inTC$ypVp&a+FkS{5vp1pxh&yTX$`vH7vd;b$Sn&nj;b?y|@MF6? zG0)se-rwX$ek#e5(2y29FvpFIDU4I*-urcw2HeZIfL7JnPw*H3ZoNJvEep+t7xuJ2 z$!EImLuVg}TDudl{Eh2;nfLghdb22%%ypIwE44mzo}A0vSsj;oLH-yR;GkA_i+-i? z#nfrPg!0@SKVj@Y2C$2jcFmUq(-z;+I1FD;n!dls2f96@0y7N=mbq5L*LDsHWiCkU zhn47a4VzpLvKW9>waJ|u?Kk}`c2a_2n!UPmlAu#g-Pjf1lE;S7DmN`-dfTNq_7Wp4 zMUEM3xIdB|5aX%nG7T?5^xcdRn8m+{FiV!fGyVvBbOpV&nls+r&qLnr@?GB>b=T1J z7-K!_<4L@CJUc_?g2tm)0HIn}i^Stw+OAMGTVy}m-ghEof=YV|0io^;YNAy8pLB;2 zb4L0H+OeV+X)^jvkBLUv@1*{=Jm-1JF(u1uH!Nn1a!Amdp_)(s1TKjCz14H>_+l8Y zcc6YO;2S)Ms=YDRul!(nV&xu> zWgNI9Jepu(g#8uCkE&UdSNqOCGCT`z7WR<*&kF*ijd?D@)|UAdF?)K9T9j|ptXTj zuJckAY3=pu@Z0RTV(;_&<-4ANq5bwz?-9m^#n!%B^|^1KA3Eu@zsW$TrR4@=sj|+y z%n-`6wFRsLPsV=3Br0v-yq>ETZQtyCzE5AX2yZ>9(*N2zB~aV$Tj!GL`y&t|XQ~Wn zCHP6O7YO$K>R<7M>B#Q4fVt1kZH}5-?_X=*UyCi-8>M;08NM~CH^s8rOg&5F6z#<9 z`9nyqNHfqUAL~7&z1y1KcQ+XBo_4V$I!RH?w}0uCXZ?`KRcpxR{||{exRo{j}we zQ)j?yYq31t1-gn$ME_xF6?&n+d(d3dE^lGQ@J{9AZWB+p!`pdE;GdCE1(3q<)?i;c z+$gxY---Sa<%ltz>pwT$-+U;-_s7u|3!|gg5kFn`7U=g+KXBi4*?XAZq6-PZBumSk zXIQ4zM@7Cpy5cI=w7u+)TA8**&%*XT-+v?%ZQJe%A8>_997F;lCm}+|xr{mY5_Ray zCyQh}yt28WUja=16G6j1>BO*Bg88o3IKkeP?{<8DV(7Cz8JZD9U)jvcmAyV1W|{+P z7K?o5rv-FW^O`*M*7VPS!YsDB$uC=ipcfJ``{dik^q=@p9^{WJj(wrXi#lpyuMGSd zE}k6@gt2l##}4BI_RT(^FBwSf%X7xS?pbNcpPSeDXi;CZ7BB3dmV@MQv|33y>sOV; zj)pI4L0d)e%rSZ*xF*GJo0O+Z@_QpB%}s}SY@LD^uHiT8zp?_(o|VDi0yzQQlvSbQ z?Y#r4MCie%zIS7Z7vo6x+%$SJSs%pdx%VDIR*vq(vE*g=fMKJFh|OX+;v=L@l}KdZb)bZw)yzf{_^}^mJlqjf+3T$Vuc)y1EX6b|W!HPYYy& zflJ$4x|ta6{ojQ>p-BpQHJn=9`e+M4foUf;7*aos!Ogs(-&%tb;cuAvQf{2Y&3<_k zyW}Z#MA&!TbjD_$@;L7P^a>XBr#tG9C~^*lu3k^rwg_lYXi?yvkK4^4nl42a+CjOR zqsQAa^r#T>eg;KQ+KNOfjTBZ}#bs*x6jWy*gzOF7DC$s2#~3aB&rYkM06x3=!0t|g zm7>VZ8IzFGwuGPCFBaeFh99ej#-sv8I+^e-CUXos#PQ3bfaVh8%~zZv-SJjM=N8E= z`~!8%E54DgY}NItLjaqxF0PT8l+&R@ue7z(C+F*q1`2MxRecjmeeT%*%XpVV@W$K& zrgkjry&i_0iIFB7{X`#+qftTXFk*F&qLIQl;O z{aLd&kV&bfj}W*Jq2iXmCkl$y>5=Y6@Gk2nD1Zf5HO)*zzi<2KPst-Z!4n6cUr}GzKYw(J=;brUK`Zkdh;IUs{d@z%E670(|;~AQ6?gu=XWg##zpl@ zi!PdErY|~PkAo7se3Gn-H5fBT6o#o=bGZVfiQlh?2+0W4Uc@}blK@b>2<0JheQ-g+ zHhMJG%0yo71FKCiF5+p{tiC$}Cebg+0>TF9CUQdQy)pdHCCV zrpUf(HiZQ=v>h>rY=62QxwM!Ibhf3*0x6LNG#I>c1-OTZLu1T$)+sj)rhX zJ7U@`#Ic1Dw%p^{S#rd~wlkJ7O>dTP#$G?<@|zAjYAb$s(aa`H9za45SEcVrU7Wmv z#4!6^K0UDF&dMfI+p3E5Egvd?Zt-TqnI_%)?4O}bx7UvxJ@+2cQm?GbDj@5{`gD)7zE=U}H5RO<0ThCE zJ5Z80TT4k6AochGQtz8GO;OAX{&Zkvbl(vFyDS%Ijs%aTXn*R@h%7a^arlI|xX?q}iFjW_uuKBd^GSgyR>=*8nA&NbV zy(+)oxvR33^K?gwqbEtd+XYY&$S>DhOdzzP<(w~U<$mTOuE9q@*{kOquI|t0N-)hd z^bhMefE3$}_MaKvxp;k*GW8`jg(W|2eQ&O`M+SG>e}2s@YSv7g>nO;p%g(QxdY`o4 zhI~{xWX(QyIK^79X(>5Xa#fp_08+ts6W8@)2CwTA=VONtlYAPhe-T}LX?+nJ%fzBJ zYsj1{VR<_Cyn8}etKFh{9m$=b)K*0>D-{?CTu@NnniuViN%Lp85BP%$adHBwm%~f? zcrE2sZ)$CrK7U5?kukXG;0C02`WzoOek(W1Zd?hsSd_W4;JnlwW|7V;F zTFW$gXCmz)-*iixP{b^CAcs@hs^LkXhT5E>!^GZ6G4$Nb*2l+yY2;zsWInI5d?`-D zBsZ6`A!?D=o#Q(Ih982(553Up0lb_^5{n2 z+BBym&-54=7H(TCUy9lFi?rdJRA=y^f4%o<%51V4!)!KPrPj4tGP-s55EXT$t+zT$ zdO3hlQ`I!0{Kst|UW@VR;oa1jx|cl@*SI^FKIEt0|5pYYJla2fsB^$~w^^Gmec?p2 z)I&yWRKRk8`IHiFqPK|&@~m$#H7bv0mhA00(d;x+i^z`N6e#()Z9P&v6DZdK^MqsP z`7X!lDW@n82sMts$u0|&@*7*}d(Xd`>3+VGg?RD%&vDiLF$`sfpx&EW{exNmst3!8 z=k{&#{0ouO&%&;LQUy3-nS045Gxm5*-7y4QfxIBHsty}N%^ipcsX0>xec+^NAR~!w z>!sCgEYJa0L6%&z{p%)(fQ%PqaW><#T&!M8Hudymny74VKHn3S*a3o^Lc15v%rzpA z|NH82$$@x_H`mSiUOt4>3Ezzl1KBLvVli$+fj3W?)bf|SKhP%Pm%6Juu_>}G?g+}~ zrnLkuc@;(?$INd#Ie#VM4Cigv;WP(u4m$-$vDWdF>u7Iy< zo*wTvza3U&%5c=X!Lc7Ivv`2Hw!QGPiyoRC=BeK0 zyq%Wd<1BypSs9CcTtQ*!)IHXtp(jgfxF8Uc8e(U_oD1M}1wV98B5sKp25b9#&H*hz zLP93}W61ct7<#ehfv(US?Gi9LQffuyjr(CbJR5&9tj~1PWTa`b+)!3(Ga@6;_}&M1 z2YCwT_wqyS;7Kl>JJoY14OEsX77;JClFgp_{eCc%@ZKkuaaU0(C3awCH<5ho2L#vE zdx%=}NgC%|;r|T6+!ah+)cCASdg$cnON*jH2Z!_^I9EEgduQ_akTi|Y{EP}de8(lr zGA>rhTBvbU3SSK~$*w_&5q`3x!kyHj^R0>xs5Y}6e2@!rC929>_SS)wJ(qJ^gC2bR zBmK*WPm!DFPeF*%uW}LtR=*sL7>RTw0*;I>xzv!DS=-ve6DJ@O>hzDzd-6@q8}bt# z4YsOXt}w-3!d5-pcEwfwgj z7M@C-F3=(~#y8zv&g=nPvSrZijSZ$bRGO4+!Z(RpVMuLC@?q!=M;+Kf0Ri9+2h``=WpsQ|t};8gKNmvimHX z>&`4T)gB(p^>O-Y!(OXa9!e&E&!1^Mp$_!fA{PS3e}Y3-?@~HNZ(G%038g+esz9Nc zF<^r-TGJaM3D${LCXy@ce+m4QB;-I`U4qeR`!W}#}4@4Ym> zj7&T1xdA#haLK$nDkQy#`K|-Oe$nSz{6pIf4#Bgn0VfJBDpN)SzpY!oGV4d4)&FA| z1Aywv&%*Gp<|$%^ww)XSdx1Pg^n+JGf%)6$2VtrAP!sYDfOjWEs6&nyo$7GhRcQ!k z6UWjszjV_;cV@nBZ2)4vzOwgJFQ&nMdz%oCd8m!j(X*Yjyv_@#a9c7~G3EA`s+mhN zne>gx`A!}w%@e*LN03T;FA*0gBj2O@B|Gy=W>OG070tE`*NB|BIG-Hz@vAD1VRyLV zBLf1v6_`b5&ru^^1NDbeKd7CPRAT3f#HFIgjb)`ClNsChExHLcPZ>203=VxG57^T5 z58SxvjRQ5JYvEJeK%X%WXk!WFNIMPqJmIZMkq8L%{_T(QwL1YrK zJNh0mVuGE<588>>w|QFlAi>DLrwy7G9eWCJ@TR`{46FG=fUvf7)XXo zwGS$q3wzDiXDstm8InT)IkRLhU~z17YEU_}9>&+yFbX=#t~z0>DoHxIuFsXKNy zIjJv?CJnfz@`;R;o>@}|5XBV5Swjfp+N3?7iY-R77KGcz6gJeEV4b(v_$|K?7JGIz z1FNVcG`ri9b(e6mPrXJmTT%T!m{+^SK#~y#_ml!IiJ?@0=_-1dBe9bh0F6A_mduiA zJn>08lfIfzRRNkBBZ;(-S$FixUF?BprktWnO5miBO9QkzFGGB$5*9MO)s^NWrl(r` z1~;jn9d0%$c*$;JAo%!|VsYajr1>?|?R-{LMsx0lF(?KQ%1%LI00}NN5>h%NEqh5? zeoQxB&(KBTykzNNISOlOQCa1fLdfZW@KyD`)-^yRBxBE&uQC?Zai_A8N%RZYSFxGq z+ZRSuc#}c2mi<~&0qLgX{>V|yh@N`El4pF;J2XOio&uMAf#z5pg&s8OtcXCz4{7S^ zEP=P|o-BiMKV4O0Sl+Z8#u-d3ghN$xVl)IdFqU)4D2y@DFWyO{;652mgbr;tcSAi$Y zye_79&(}PxKWy~!_nJ50x2zoJzznV4FzQmE8cDIW4AA!!eTzwKyzoH0OA;tAA9A9Z z$iIDig*uRr_9{BreMy(`$(rEI>irt?%@-_KSEmGN73Pj?f{;#JG5a-R>FKlXK%-F; z*Y2@Gu(hG*Sb6TafOzZfLO`h2s627^JZoiC_p2HZ0@FZ6HP%1K(SU0X-k>M%qU`q} zOVmM3Z`a=)pvSF@w?)g^&rxq5km=|vGt2NlyC6ru9q0O{D&8iHSHebN6!934BP@$Q zhPMeE^(gf|KjlpeIq0>zBXvab=82|Ir0u7o#@l}u@ zKO%ZJ7uOp`tN=K$+jZ_w;7b~tx%m_mIREjQP~->t&tE+Y=IngCqQ^fP$*P=K-Kn*P zgD7gP{y{1vO+!ph$k+tm-D4UE(xfB@y^o>DLM#QhrE|f$g||yLW;PJE>qw}^hH_k( z9;CZ!?InFBsCkWY(d(FdL26Kjye`b*s|f^_z`Jj7?=rg2+*$%6{?o%dhnHU&pt=@(n)W_bH8W{cjt+S}BYTh?Dox7L=Rle^Wt zuq=K=kx@+)&J4{$l9ygWH^2EEXp&hn9e0`gY}7z?#&X6UPbnbfs)8!{95v%mxvpw2 z5$Fb>Q{fz*lKjb~KQ1*t?+rxNpgckq>Vc`63$Itmbz64<-iapt^3fcJO9ub#TMU=1^pdig8@xM=B)27L>kZpk}BD4)jv?|H%haHjlSLs z`6S#pTXt3I1;k9^khtp=qXfG|kV7wFG&gsPgE!u$@y7gQsk|me{U2u7iWB(jZuu&9# z?3#F)kVyek>%AH|o^#FwF3vDE1La?jOQI_1zqPEA{Q-!&b!{9WQRh?AaiLXd2qdW^ zTn{{tuto-}uK&X9M3|28-g@!dgaBW`oQG$~9gjT_$CGt}rl zqP6mgs98B#+vVCvTh`mNxT@x6P8OlWe@a9bpWZ#<&}hTSiZ6y3MPo(eb8mXK$3*4c6&(1}|B?@W6W z>+vMbtuu-3{df0y%0)WvAoZQs4UW^~cZUm8 zP}RNL&nakvG`(HMbE1o`Mg?OlH}icS~Fj|sV4iItoHZYYNao4Fu+T39_h z#1ehwqU!lrdvdR51T+ZQuKi&u(%rd(o_)tc+IQpBr9N7@CYVxNWk>xgeL^ZLl{*5s zkEud?6Kt6CCKH(k5{L!S-tfJT0krvmt3gbD(bG4s!_~~HsL<;OTI9}xfEQgNU3NVF zqbj~zRoX1BJJmpvqMhk?1@Ln3DR;2LT}5HXzP*r`hsRtJ8qbYm)%|m@OUs^oNlRq{ zb>%ePRfy{dMHd){IXQ7_PhETBG-@kWFK!Vwc!ALxvz<*j;JoAgOAavieyx@Dz3_!o*DkuB)K9lDue!8R}xoZztAvew1@1jNdU`DsL6* z%VMZh4P?E&G~0HCHn!#4JsI31vTM{mF}F?1I>%XH@e(YKqG-L%2m) zFLN0{%MhB-^NZ%XnXV4xMt4>SN#u+A@{LJ7>H^X5xysCdz&w zn$!T8a?g?jCsus)dC*~^cub3ix$srArN}u=%k_aIxw-9B-km=AJ^@wkfSz@lo?c#Z zhFzSi>GdZ)4YTLs4xFz*j50I5?n=FF&E}NMPQiaG1t_ZYUw}fm1f+ zj%UkFgDvZoqnzFSE02$Hr&o*5Xbf#1wx`R#V?F4+opIRpiDa_m_vgZ_k`WRU6@K_| zo-7 z)o$QgTDQ%`hz^-``%sTzn7*d}I69kZj85mU(?JJ*0ua~mD*pWw4>6ZYX6t0WJ@zZq zN3Sbbn6xs1`84 z;R*N$i}$91DJFexdQ%`tv3LXK1D(xA<~}JzxrmO3DHBt#%k2o36bQbkKl#b?(AL?) z-IJWpLRUww`5Ca%^L0v|s{QQTF`vp4L`G{`pc0Kuz3pgC{Cxn`T z5_`MK^s|!|O{ErhLpldFb?c6la`(f;Otti$v@Ra}ss%l0cj}SchxA8^gbn6i&_=A; z&u`8d6~0qcpbUK6JNAVqx@l32qNI5@6qyDZ1~BWQZAlejaTg7G==~z>Yd@_WLSTd9 z0sLRl+3jky*kk%#{VAYv-r>huDICTL^|uVr{4J&F0K8Xh#>kV96Vmua)%MAeBpS%Q zUn%bA#4QI)b&F-K8oElNIvl5XW9p4=%WcUcTnV6Bb(zW;I$3C{?YV0jp84*&&zew8 zR2_ez_q|q?mx>2^->`hoWmRS)QZhDN6_1NPinI3J@{9~uEHxW)F;;1Ct{DG`NATk{ zds^OJYl}UDWjyFN6QK~Kb*|bet~xsD2qGE5J#~1`wMF zUbhAV%O7Sz*G@ARe8qmfpEN<;X&JU8V(!Jv=G4Y!hV~0@lRQ6oswHMn$*XGtsAZ0k zJWxaDcp(euk0NYhG&>bQGS$S=l#^y4czbf}pHPtNprg@mNls#6Y_#P9dy^<$YO@uv zCB>v*;~w33%$>+%+%4~Gn~af1237y{wgq{-l4iwlF8FB z`}P&7UJ7@)Q~pxN8s+NsyVgc=$_>^&$wSvhn}-i%B@bOrY^ACI@#6VwUAsatSdgc_WoDh)=s~|gbVU&Q2+GfE&Gk; zyT>!~IT6O&X)J)NcJwym{?|7w?({XVS~6G{Z%dk!-_sfc&1VBvqw7jk8uF)HoU9O?Nj0lXuT7*ng*F@3iommzVJL=3h6As%mCn zRycshid&GS623bD3fi&V$BlG?>uxG0(17(Z6|h31jjMI-x0}S}4xOC1gt~z*-@b)J z|K50ykHZkuGeY>{x}`--$ioR=BwvK&Rvo5-UK3UeS3)Qa%m0-eeL1s97yRorN^4ez0r{bayus0nvVk1u?6-x% z3XtBUBQD;Gpi{i51(Bg72f6c6w_Q_NP{d;eV4is9jr6&ZS8Z_2WFrIKi~AvoS38N# zn^pH)_Dx08KUh%u91ZJXhgp#-|WCY9}F zIFqx!lr^*y-S7gz)f42c`5`!haPk${9z({!y5x6YEV{{Vg8xpQ7r4(o)=YM{`OrQ9 zc5P&uD6SZQN#Ofv{%Tg*ZI*v8O6OQoZ4#hw6*pq6gJZ5gJSFb1?(7lGrN67p;+V&^ z^Yn!Vn6~gVPt~<2-|&2|yW2W4AF<@jc>YSAYoQ#cGNnmQI<9+dCNGz@`3Lqnl!zVE+v*gQfs>yfBw1E zgy4NZl?AqSikMQ%k;mc`od*H)}8* z!I!sii$4bo&Pwk6+1MRoN5wiqVbKR7Vk(d({>Sl|BEgJ^#qfo=OzAwUGZp_K$FEVj zRYxcqO1xAjx%JZ|C>&#SJdM&xC{O8#>Y3769OS}1z4p&}xH>+%E7Dw5~UOcoYAbZY` z6*c_o6=_ZTgXe%Nx#fR~(2~4XN)d--Jy*F6-uypJvL=Mhq2&^diM{#39e&fX3gj_h zl@Cdil=f2cbd1ncQhwf3M$DhumA$NiX(o1*I8o!fEsJiBg>hm0oOH&SSwLCl`kBqk z$%L$B$~G3=5qonCX5&DHw!2OVa8sKdPpSReS6sS8`vu3e#;TZ$hc#^AAr;mr}tq{-uq#;*?iX zW&gdvkfIpTr%R&uuP{isOywU!KI(4WGIhZKosC3JnwcJ^Sg7&`^1RH^V0ya0y^B+! zFi;4;EP52VmEJk%mKO1KOFV|@Ahd(I5g^n6Gu6M*$B?R3f(%8mxHHbW)!-Ft&jCCE~=b6Y_Xd9uPAwIsIMRT@L*Hv3-CC z&^NH^W>_DXZ8^*8F$TjmyyQ7?;L18b58H=@|Eq|I(Ne+kGs33U7X-nWAY`g)^?x|c zzZ_sqS6me@EYWC79(^mWd!kK|a-ySdMaE@n`Vf_;p6%|oXuwr`dG6HCf{6e3OEjA} z;wc3MP~wiRzBzC+t~(v1=*+xSFt9cs&WjWZVx_%|K!LaWYZF4kzI4IJgTl*j=Ks7 zOacKad=2SQ?e9cM9*d;sNx*b0~thYE|75tR|ua z3_hu#K89m`?d-^r;*C?DVj&!H;C{4ae0ks>-_Z!R(Y=SGFFEPW6<$b6(Pail6>CI3 z()<*5R@3nH#J*{yffbZ*_uao8&?uqHeW54^=MYpd1{)6D&gK`PCv~Q#b15m@o#Qw17Pa@H-6*48P%j}X}8{K zuxY220Yu3^tomsE;CVSA>4K14jPLo}ZF$%Ip2WVuz($yV0u}J)p@pTOwYWgcK4l#9!8_8HI;Xg;X znzf|^NmCV_JPb!TA97a0l1w`uk-RpzAn!83E}&DdoykIff~;acJH;V9E{5ck6)}T= zC63Q@dI3q@&1(5w(aftGaR#0nLbyH<)|8j_)Mi_JuNSo4f!NMA2SxEL7 z)i0M&r|{PIytt9lH6A!KIA*%WQC$6T`!vR|RNCTR`f#XHK?n8L%d+ z^&C+lyT@FewB$i1FCS(kI^Ilw4HkjD|CgB!TxP@udw_*pt1nra8Eo&VZu|yJ30h3Tx2L7lS7P{t4h;+8l6WdXH1OM0e6mor!3?NEqU(G z?}@?Js}=txiv4%5k zz|R17yWMhXB&l^y6*45ghwFWPJ|jctNA+!jm9-a*-rGaVGM}Wsah|f@&nCbYwa{y% zdowOmnU>@$+7B7`lM)c5sX6X(h^tO&c>nFT<$&ZgUdWtIOttO*o8%$~6Ax0A?xMN$ zjsJhWq|5#m7@7`b0eAHsLdg2z?BitU;DxOzqRzZkbipGgS|> z!A7*@W0#KCa6~&2;aJydB*+oLc*VRdrAP|Kj=FDeOy*CDYkGmmjh3s;Z~oN1c>R|@ zv{U8&KPHI%eK4t6<8CWl&(6%8d^QiP+T#d54ZNljo=a8!Lt7Po3M{_(rjQ4CAIS9j z@Keg~oG$SZ-HxAS44JUgQ>S9NxF za%gh?+fCFech_nr6EfG@8mBBM3X9&$F(k=~hBHyGbU|aW7k@F}g7TIbs0QvGBF~PH zyEi=9<*~?g)_$x)Xy@@;P&qx} zn~3LmIM+%x?P|bi$VJo-p~6t_~FI7QnH-VSOML6EGq~_Bstmoln<;L7s=|cKEYW zgTa7DG@MkRb8+o*A_|PE^5t$znC5cIkZWSuU*UgqBAFX6ZGDCjkJjxG8J<@LGtq8d zBE>!pb;RJ%PJ(**A>4C{Xf)1s;RXlwi87o5DYGPwm;^BW(&3@Rs?_3=ECxiNXIgnF zueZ0vnPv*6A-8LIDRV48^?yUzIFT&@jpc`X*eL}WI6<@xCs?$}LNN~wL&W^Zs64#E zRlrF3@TJ2cN^hIRYugmo++HqA65z-<-7%mCUgT4t7Y;Ke%1x<%Y#F5dgWv@z>|S+b zpgZ}``yily%jhwYgOV)Cx6X2Q_iwr0*KiTkoa}oqNkf`jsjBt;+Qsxq=Pc=#_x<1TshhsY$&qyVSXpAmaL7!5HRdbbfN<~jsJulUx!L+zVS z=Q~}cd$?}d>PjxQS(|iSSoPb-Z<;lU{R6rTmrNh<=0?xhJT|fW3@jk2yQSg>1@D?h$Adj@GH$h+;tD z-QH~k#==p$vA)qZU-5jj!}`Cj&~P~QsONubtia+OYrgd1egurdHMI+CzBBq#E0&Ax zbi;%m%*L7cuCfY7Ynm~``K#rJ^fmyszv@UN{ipF>KlJsTu1m%iv`6QDWh{F6sz!%; zH+22lGAl(MQ#`c82+K-5z<_Ye-stj$?-;hsQ+i5bf3E97B*RD|?SD4Xhj&))jXu+k zbv^aKH`+-e7Gv$^h~P6At!QJ28CZe-f1ToU%LGLTP~}h@MnH^};`=9CNmu|lnOpNB z$Y2q?klU6SiMh3tVX%MMylejnCy#3gb*=uTyHO1Pd75`{%nv+128+bfY*sXNPb8gh ztA`!@)x7Yy62Ud-+s;ezyffHOVC11}!{2S3Im~C|#~ve4<`aneVsKrY^nB4U%X%Vc3_m{e>);@MmChK6!;cCH($DEi`-i!`%^WZx*d7pqv_LZ1M; zsxPE7-&C3{ZL+`O@Uu|$&LusJyx(N}8kNX|?=Pi}%S?2J{B7&@UzOvYm(8|*{t*g2ZW+XrwOkNg zC$-@O?L6Y#1E(fYvN92Qd5-)@f5&Fodn^d4AcAH-tG?+3}Cfa;@#Xt)u2IA6>Y9{KMk=rTSkE+;czkM%Pa;6?Z5(?mW0s zWyMY)Vx(K>Kl%(QfWy2g zsOZ~d26409#-AknxN7r1j)I$Vu}HV6S@S)Wt;7t+KzcRno;4eJfFd_zFQ5Uz-n3%O z8^={SrALa^rN+7nH5@>8zOpZWD?-BXKI<_3lZ4AZgp*x=aZ^ViD^mwHq6Z5o569?i z=Os}?c_Mko?RDnimu=5p3U22x(Nh>s6>@$~sF0bx!BS|~Y;fZzx8NjwsqoKfXyP3i zxe&%ZV;1A9>j_E7WpF;q-S?^)W0Jo1d0CGg%F;{uI^g4TsW#*;h_D0Cs#JQ|cm9VO zguNV)T>U&>(TDkX$3Orgxs@72?3l2JlW2>IbN>+NcGx&ZC9|=H)#od5z7Xp z`5rXho;5K-2}99YOzaTutn%(+q@$58ch<6#A2HMs>n5?9rNn^pl-p0#ILY0tiOkI` zXaq*{5lu;Q*~PVp#?AlB11cf8x;~hrE|cY&^mSKq)x_3zKlW;01Nm6^+jBBxO@-}J~NP(I;xVta(QHj|E4m>H%FcJb0R5kk~N!q|MI$FdPqmiz{-os zd4I`O0q*UKM!Jp7n$d$ns$*aQOI705M84!|;xIkmzqj6Fqn-&QLR}Y>8HhjS9|o+y z)2*7k0GoMRh%U!!om__4@EP0k@mG~-LC^31?PpE{q(m;I^f|PJ5duCNbIK_}z-PIe z`oPb_n>KB5dRI*MI&IGrmcCI NSz{}s*9LBJ{|m(J9Bu#r literal 0 HcmV?d00001 diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/main.py b/Source code/WINDOWS/akamura/ciscomerakiclu/main.py new file mode 100644 index 0000000..b550e5c --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/main.py @@ -0,0 +1,196 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +import sys +import logging +import traceback + +required_packages = { + "tabulate": "tabulate", + "pathlib": "pathlib", + "datetime": "datetime", + "termcolor": "termcolor", + "requests": "requests", + "rich": "rich", + "setuptools": "setuptools", + "cryptography": "cryptography" +} + +missing_packages = [] +for module, package in required_packages.items(): + try: + __import__(module) + except ImportError: + missing_packages.append(package) + +if missing_packages: + print("Missing required Python packages: " + ", ".join(missing_packages)) + print("Please install them using the following command:") + print(f"{sys.executable} -m pip install " + " ".join(missing_packages)) + sys.exit(1) + +from getpass import getpass +from datetime import datetime +from termcolor import colored + + +# ================================================== +# IMPORT custom modules +# ================================================== +from api import meraki_api_manager +from settings import term_extra +from settings import db_creator +from utilities import submenu + + +# ================================================== +# ERROR logging +# ================================================== +logger = logging.getLogger('ciscomerakiclu') +logger.setLevel(logging.ERROR) + +log_directory = 'log' +if not os.path.exists(log_directory): + os.makedirs(log_directory) + +log_file = os.path.join(log_directory, 'error.log') +file_handler = logging.FileHandler(log_file) +file_handler.setLevel(logging.ERROR) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +file_handler.setFormatter(formatter) +logger.addHandler(file_handler) + + +# ================================================== +# VISUALIZE the Main Menu +# ================================================== +def main_menu(fernet): + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + + api_key = meraki_api_manager.get_api_key(fernet) + options = [ + "Network wide [under dev]", + "Security & SD-WAN", + "Switch and wireless", + "Environmental [under dev]", + "Organization [under dev]", + "The Swiss Army Knife [under dev]", + f"{'Edit Cisco Meraki API Key' if api_key else 'Set Cisco Meraki API Key'}", + "Exit the Command Line Utility" + ] + current_year = datetime.now().year + footer = f"\033[1mPROJECT PAGE\033[0m\n© {current_year} Matia Zanella\nhttps://developer.cisco.com/codeexchange/github/repo/akamura/cisco-meraki-clu/\n\n\033[1mSUPPORT ME\033[0m\n☕️ Fuel me with a coffee if you found it useful https://www.paypal.com/paypalme/matiazanella/\n\n\033[1mDISCLAIMER\033[0m\nThis utility is not an official Cisco Meraki product but is based on the official Cisco Meraki API.\nIt is intended to provide Network Administrators with an easy daily companion in the swiss army knife." + + # Description header over the menu + print("\n") + print("┌" + "─" * 58 + "┐") + print("│".ljust(59) + "│") + for index, option in enumerate(options, start=1): + print(f"│ {index}. {option}".ljust(59) + "│") + print("│".ljust(59) + "│") + print("└" + "─" * 58 + "┘") + + term_extra.print_footer(footer) + choice = input(colored("Choose a menu option [1-8]: ", "cyan")) + + if choice.isdigit() and 1 <= int(choice) <= 8: + if choice == '2': + if api_key: + submenu.submenu_mx(api_key) + else: + print("Please set the Cisco Meraki API key first.") + input(colored("\nPress Enter to return to the main menu...", "green")) + + elif choice == '3': + if api_key: + submenu.submenu_sw_and_ap(api_key) + else: + print("Please set the Cisco Meraki API key first.") + input(colored("\nPress Enter to return to the main menu...", "green")) + + elif choice == '7': + manage_api_key(fernet) + elif choice == '8': + term_extra.clear_screen() + term_extra.print_ascii_art() + + print("\nThank you for using the Cisco Meraki Command Line Utility!") + print("Exiting the program. Goodbye, and have a wonderful day!") + print("\n🚀 \033[1mCONTRIBUTE\033[0m\nThis is not just a project; it's a community effort.\nI'm inviting you to be a part of this journey.\nStar it, fork it, contribute, or just play around with it.\nEvery feedback, issue, or pull request is an opportunity for us to make this tool even more amazing.\nYou are more than welcome to discuss it on GitHub https://github.com/akamura/cisco-meraki-clu/discussions") + print("\n" * 2) + + break + else: + print(colored(f"You selected: {options[int(choice) - 1]}", "green")) + else: + print(colored("Invalid choice. Please try again.", "red")) + +def manage_api_key(fernet): + term_extra.clear_screen() + api_key = input("\nEnter the Cisco Meraki API Key: ") + meraki_api_manager.save_api_key(api_key, fernet) + + +# ================================================== +# ERROR handling and logging +# ================================================== +if __name__ == "__main__": + try: + db_path = 'db/cisco_meraki_clu_db.db' + if not db_creator.database_exists(db_path): + os.system('cls') + term_extra.print_ascii_art() + if db_creator.prompt_create_database(): + fernet = getpass(colored("\nEnter a password for encrypting the database: ", "green")) + else: + print(colored("Database creation cancelled. Exiting program.", "yellow")) + exit() + else: + os.system('cls') + term_extra.print_ascii_art() + fernet = getpass(colored("\n\nWelcome to Cisco Meraki Command Line Utility!\nThis program contains sensitive information. Please insert your password to continue: ", "green")) + + # Generate Fernet key based on the fernet + fernet = db_creator.generate_fernet_key(fernet) + main_menu(fernet) + + except Exception as e: + logger.error("An error occurred", exc_info=True) + print("An error occurred:") + print(e) + traceback.print_exc() + input("\nPress Enter to exit.\n") \ No newline at end of file diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/__init__.py b/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_api.py b/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_api.py new file mode 100644 index 0000000..475cd5b --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_api.py @@ -0,0 +1,387 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import requests +import subprocess +import sys +import csv +import os +try: + from tabulate import tabulate +except ImportError: + subprocess.check_call([sys.executable, "-m", "pip", "install", "tabulate"]) +from datetime import datetime +try: + from termcolor import colored +except ImportError: + subprocess.check_call([sys.executable, "-m", "pip", "install", "termcolor"]) + + +# ================================================== +# EXPORT device list in a beautiful table format +# ================================================== +def export_devices_to_csv(devices, network_name, device_type, base_folder_path): + current_date = datetime.now().strftime("%Y-%m-%d") + filename = f"{network_name}_{current_date}_{device_type}.csv" + file_path = os.path.join(base_folder_path, filename) + + if devices: + # Priority columns + priority_columns = ['name', 'mac', 'lanIp', 'serial', 'model', 'firwmare', 'tags'] + + # Gather all columns from the devices + all_columns = set(key for device in devices for key in device.keys()) + + # Reorder columns so that priority columns come first + ordered_columns = priority_columns + [col for col in all_columns if col not in priority_columns] + + with open(file_path, 'w', newline='', encoding='utf-8') as file: + # Convert fieldnames to uppercase + fieldnames = [col.upper() for col in ordered_columns] + writer = csv.DictWriter(file, fieldnames=fieldnames) + writer.writeheader() + for device in devices: + # Convert keys to uppercase to match fieldnames + row = {col.upper(): device.get(col, '') for col in ordered_columns} + writer.writerow(row) + + print(f"Data exported to {file_path}") + else: + print("No data to export.") + + +# ================================================== +# EXPORT firewall rules in a beautiful table format +# ================================================== +def export_firewall_rules_to_csv(firewall_rules, network_name, base_folder_path): + current_date = datetime.now().strftime("%Y-%m-%d") + filename = f"{network_name}_{current_date}_MX_Firewall_Rules.csv" + file_path = os.path.join(base_folder_path, filename) + + if firewall_rules: + # Priority columns, adjust these based on your data + priority_columns = ['policy', 'protocol', 'srcport', 'srccidr', 'destport','destcidr','comments'] + + # Gather all columns from the firewall rules + all_columns = set(key for rule in firewall_rules for key in rule.keys()) + + # Reorder columns so that priority columns come first + ordered_columns = priority_columns + [col for col in all_columns if col not in priority_columns] + + with open(file_path, 'w', newline='', encoding='utf-8') as file: + # Convert fieldnames to uppercase + fieldnames = [col.upper() for col in ordered_columns] + writer = csv.DictWriter(file, fieldnames=fieldnames) + writer.writeheader() + for rule in firewall_rules: + # Convert keys to uppercase to match fieldnames + row = {col.upper(): rule.get(col, '') for col in ordered_columns} + writer.writerow(row) + + print(f"Data exported to {file_path}") + else: + print("No data to export.") + + +# ================================================== +# GET a list of Organizations +# ================================================== +def get_meraki_organizations(api_key): + url = "https://api.meraki.com/api/v1/organizations" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json" + } + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print("Failed to fetch organizations") + return None + +def select_organization(api_key): + organizations = get_meraki_organizations(api_key) + if organizations: + for idx, org in enumerate(organizations, 1): + print(f"{idx}. {org['name']}") + + choice = input(colored("\nSelect an Organization (enter the number): ", "cyan")) + try: + selected_index = int(choice) - 1 + if 0 <= selected_index < len(organizations): + return organizations[selected_index] + else: + print("Invalid selection.") + except ValueError: + print("Please enter a number.") + + return None + + +# ================================================== +# GET a list of Networks in an Organization +# ================================================== +def get_meraki_networks(api_key, organization_id, per_page=5000): + url = f"https://api.meraki.com/api/v1/organizations/{organization_id}/networks" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json", + "Accept": "application/json" + } + params = { + "perPage": per_page + } + response = requests.get(url, headers=headers, params=params) + if response.status_code == 200: + networks = response.json() + # Sort the networks by name + networks.sort(key=lambda x: x['name']) + return networks + else: + print("Failed to fetch networks") + return None, None + + +# ================================================== +# SELECT a Network in an Organization +# ================================================== +def select_network(api_key, organization_id): + networks = get_meraki_networks(api_key, organization_id) + if networks: + for idx, network in enumerate(networks, 1): + print(f"{idx}. {network['name']}") + + choice = input(colored("\nSelect an Organization Network (enter the number): ", "cyan")) + try: + selected_index = int(choice) - 1 + if 0 <= selected_index < len(networks): + return networks[selected_index] + else: + print("Invalid selection.") + except ValueError: + print("Please enter a number.") + + return None + + +# ================================================== +# GET a list of Switches in an Network +# ================================================== +def get_meraki_switches(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/devices" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + devices = response.json() + switches = [device for device in devices if device['model'].startswith('MS')] + return switches + else: + print("Failed to fetch switches") + return None + +def display_switches(api_key, network_id): + switches = get_meraki_switches(api_key, network_id) + if switches: + print(tabulate(switches, headers="keys", tablefmt="pretty")) + else: + print("No switches found in the selected network.") + + +# ================================================== +# GET a list of Switch Ports and their Status +# ================================================== +def get_switch_ports(api_key, serial): + url = f"https://api.meraki.com/api/v1/devices/{serial}/switch/ports" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch switch ports for serial {serial}, status code: {response.status_code}") + return None + +def get_switch_ports_statuses_with_timespan(api_key, serial, timespan=1800): + url = f"https://api.meraki.com/api/v1/devices/{serial}/switch/ports/statuses" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + # Assuming the API supports a 'timespan' query parameter for this endpoint + params = {'timespan': timespan} + + response = requests.get(url, headers=headers, params=params) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch switch ports for serial {serial}, status code: {response.status_code}") + return None + + +# ================================================== +# GET a list of Access Points in an Network +# ================================================== +def get_meraki_access_points(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/devices" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + devices = response.json() + # Filter to include only access points (APs) + access_points = [device for device in devices if device['model'].startswith('MR')] + return access_points + else: + print("Failed to fetch access points") + return None + + +# ======================================================================= +# [UNDER DEVELOPMENT] GET a list of VLANs and Static Routes in an Network +# ======================================================================= +def get_meraki_vlans(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/vlans" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch VLANs: {response.status_code}, {response.text}") + return None + +def get_meraki_static_routes(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/staticRoutes" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch static routes: {response.status_code}, {response.text}") + return None + + +# ================================================== +# SELECT a Network in an Organization +# ================================================== +def select_mx_network(api_key, organization_id): + networks = get_meraki_networks(api_key, organization_id) + if networks: + for idx, network in enumerate(networks, 1): + print(f"{idx}. {network['name']}") + + choice = input(colored("\nSelect an Organization Network (enter the number): ", "cyan")) + try: + selected_index = int(choice) - 1 + if 0 <= selected_index < len(networks): + return networks[selected_index] + else: + print("Invalid selection.") + except ValueError: + print("Please enter a number.") + + return None + + +# ================================================== +# GET Layer 3 Firewall Rules for a Network +# ================================================== +def get_l3_firewall_rules(api_key, network_id): + url = f"https://api.meraki.com/api/v1/networks/{network_id}/appliance/firewall/l3FirewallRules" + headers = {"X-Cisco-Meraki-API-Key": api_key, "Content-Type": "application/json"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json()["rules"] + else: + print("Failed to fetch L3 firewall rules") + return None + + +# ================================================== +# DISPLAY Firewall Rules in a Table Format +# ================================================== +def display_firewall_rules(firewall_rules): + if firewall_rules: + print("\nLayer 3 Firewall Rules:") + print(tabulate(firewall_rules, headers="keys", tablefmt="pretty")) + else: + print("No firewall rules found in the selected network.") + + +# ============================================================== +# FETCH Organization policy and group objects for Firewall Rules +# ============================================================== +def get_organization_policy_objects(api_key, organization_id): + url = f"https://api.meraki.com/api/v1/organizations/{organization_id}/policyObjects" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json", + "Accept": "application/json" + } + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + print("Policy Objects:", data[:5]) + return data + else: + print(f"Failed to fetch organization policy objects: {response.text}") + return [] + +def get_organization_policy_objects_groups(api_key, organization_id): + url = f"https://api.meraki.com/api/v1/organizations/{organization_id}/policyObjects/groups" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json", + "Accept": "application/json" + } + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + print("Policy Objects Groups:", data[:5]) + return data + else: + print(f"Failed to fetch organization policy objects groups: {response.text}") + return [] + + +# ============================================================== +# FETCH Organization Devices Statuses +# ============================================================== +def get_organization_devices_statuses(api_key, organization_id): + url = f"https://api.meraki.com/api/v1/organizations/{organization_id}/devices/statuses" + headers = { + "X-Cisco-Meraki-API-Key": api_key, + "Content-Type": "application/json" + } + + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch organization devices statuses. Status code: {response.status_code}") + return [] \ No newline at end of file diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_ms_mr.py b/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_ms_mr.py new file mode 100644 index 0000000..4e9cb0d --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_ms_mr.py @@ -0,0 +1,228 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +from datetime import datetime +from termcolor import colored +from rich.console import Console +from rich.table import Table +from rich.box import SIMPLE + + +# ================================================== +# IMPORT custom modules +# ================================================== +from modules.meraki import meraki_api +from settings import term_extra + + +# ================================================== +# DEFINE how to retrieve Switch Ports data +# ================================================== +def display_switch_ports(api_key, serial_number): + port_statuses = [] + + try: + switch_ports = meraki_api.get_switch_ports(api_key, serial_number) + except Exception as e: + print(f"[red]Failed to fetch switch port configurations: {e}[/red]") + return + + try: + port_statuses = meraki_api.get_switch_ports_statuses_with_timespan(api_key, serial_number) or [] + except Exception as e: + print(f"[red]Failed to fetch real-time port statuses/packets: {e}[/red]") + + if switch_ports: + table = Table(show_header=True, header_style="green", box=SIMPLE) + + columns = [ + ("Port", 5), ("Name", 30), ("Enabled", 5), + ("PoE", 5), ("Type", 10), ("VLAN", 5), + ("Allowed VLANs", 8), ("RSTP", 5), ("STP Guard", 10), + ("Storm Cont", 5), ("In (Gbps)", 8), ("Out (Gbps)", 8), + ("powerUsageInWh", 8), ("warnings", 30), ("errors", 30) + ] + for col_name, col_width in columns: + table.add_column(col_name, style="dim", width=col_width) + + for port in switch_ports: + port_id = port.get('portId', 'N/A') + status = next((item for item in port_statuses if item.get("portId") == port_id), {}) + + row_data = [ + port.get('portId', 'N/A'), + port.get('name', 'N/A'), + "Yes" if port.get('enabled') else "No", + "Yes" if port.get('poeEnabled') else "No", + port.get('type', 'N/A'), + str(port.get('vlan', 'N/A')), + port.get('allowedVlans', 'N/A'), + "Yes" if port.get('rstpEnabled') else "No", + port.get('stpGuard', 'N/A'), + "Yes" if port.get('stormControlEnabled') else "No", + f"{float(status.get('usageInKb', {}).get('recv', 'N/A')) / 1000000:.2f}" if status.get('usageInKb', {}).get('recv', 'N/A') != 'N/A' else 'N/A', + f"{float(status.get('usageInKb', {}).get('sent', 'N/A')) / 1000000:.2f}" if status.get('usageInKb', {}).get('sent', 'N/A') != 'N/A' else 'N/A', + str(status.get('powerUsageInWh', 'N/A')), + str(status.get('warnings', 'N/A')), + str(status.get('errors', 'N/A')) + ] + table.add_row(*row_data) + + console = Console() + console.print("\nSwitch Ports:") + console.print(table) + else: + print("[red]No ports found for the given serial number or failed to fetch ports.[/red]") + + input("Press Enter to continue...") + + +# ================================================== +# DISPLAY device list in a beautiful table format +# ================================================== +def display_devices(api_key, network_id, device_type): + devices = None + if device_type == 'switches': + devices = meraki_api.get_meraki_switches(api_key, network_id) + elif device_type == 'access_points': + devices = meraki_api.get_meraki_access_points(api_key, network_id) + + term_extra.clear_screen() + term_extra.print_ascii_art() + + if devices: + devices = sorted(devices, key=lambda x: x.get('name', '').lower()) + table = Table(show_header=True, header_style="green", box=SIMPLE) + + priority_columns = ['name', 'mac', 'lanIp', 'serial', 'model'] + excluded_columns = ['networkId', 'details', 'lat', 'lng', 'firmware'] + other_columns = [key for key in devices[0].keys() if key not in priority_columns and key not in excluded_columns] + + for key in priority_columns: + table.add_column(key.upper(), no_wrap=True) + + for key in other_columns: + table.add_column(key.upper(), no_wrap=False) + + for device in devices: + row_data = [str(device.get(key, "")) for key in priority_columns + other_columns] + table.add_row(*row_data) + + console = Console() + console.print(table) + else: + print(colored(f"No {device_type} found in the selected network.", "red")) + + choice = input(colored("\nPress Enter to return to the precedent menu...", "green")) + + +# ================================================== +# DISPLAY organization devices statuses in table +# ================================================== +def display_organization_devices_statuses(api_key, organization_id, network_id): + devices_statuses = meraki_api.get_organization_devices_statuses(api_key, organization_id) + term_extra.clear_screen() + term_extra.print_ascii_art() + + filtered_devices_statuses = [device for device in devices_statuses if device.get('networkId') == network_id] + filtered_devices_statuses = [device for device in filtered_devices_statuses if device.get('productType') in ["switch", "wireless"]] + + if devices_statuses: + devices_statuses = [device for device in devices_statuses if device.get('productType') in ["switch", "wireless"]] + devices_statuses = sorted(devices_statuses, key=lambda x: x.get('name', '').lower()) + table = Table(show_header=True, header_style="green", box=SIMPLE) + + priority_columns = ['name', 'serial', 'mac', 'ipType', 'lanIp', 'gateway', 'primaryDns', 'secondaryDns', 'PSU 1', 'PSU 2', 'status', 'lastReportedAt'] + + for key in priority_columns: + formatted_key = key if not key.startswith("PSU") else key.replace(" ", "") + table.add_column(formatted_key.upper(), no_wrap=True) + + for device in devices_statuses: + row_data = [] + for key in priority_columns[:-4]: + value = str(device.get(key, "N/A")) + row_data.append(value) + + add_power_supply_statuses(device, row_data) + + status_value = str(device.get('status', "N/A")) + if status_value.lower() == 'online': + row_data.append(f"[green]{status_value}[/green]") + elif status_value.lower() == 'dormant': + row_data.append(f"[yellow]{status_value}[/yellow]") + elif status_value.lower() == 'offline' or status_value.lower() == 'alerting': + row_data.append(f"[red]{status_value}[/red]") + else: + row_data.append(status_value) + + last_reported_at = device.get('lastReportedAt') + if last_reported_at: + original_datetime = datetime.strptime(last_reported_at, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_datetime = original_datetime.strftime("%Y-%m-%d %H:%M") + row_data.append(formatted_datetime) + else: + row_data.append("N/A") + + table.add_row(*row_data) + + console = Console() + console.print(table) + + else: + print("[red]No 'switch' devices found in the selected network.[/red]") + choice = input("\nPress Enter to return to the previous menu... ") + + +# ================================================== +# INTEGRATE additional JSON data for PSU's details +# ================================================== +def add_power_supply_statuses(device, row_data): + power_statuses = [] + if 'components' in device and 'powerSupplies' in device['components']: + for slot in [1, 2]: + power_supply = next((ps for ps in device['components']['powerSupplies'] if ps.get('slot') == slot), None) + if power_supply: + status = power_supply.get('status', 'N/A') + if status.lower() == 'powering': + power_statuses.append(f"[green]{status}[/green]") + elif status.lower() == 'disconnected': + power_statuses.append(f"[red]{status}[/red]") + else: + power_statuses.append(status) + else: + power_statuses.append("N/A") + else: + power_statuses = ["N/A", "N/A"] + row_data.extend(power_statuses) \ No newline at end of file diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_mx.py b/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_mx.py new file mode 100644 index 0000000..9b83d13 --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/modules/meraki/meraki_mx.py @@ -0,0 +1,148 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +import sys + +from pathlib import Path +from datetime import datetime +from termcolor import colored +from rich.console import Console +from rich.table import Table +from rich.box import SIMPLE +from rich.text import Text + + +# ================================================== +# IMPORT custom modules +# ================================================== +from modules.meraki import meraki_api +from settings import term_extra + + +# ================================================== +# DISPLAY Firewall Rules in a Beautiful Table Format +# ================================================== +def display_firewall_rules(api_key, network_id, organization_id): + rules = meraki_api.get_l3_firewall_rules(api_key, network_id) + policy_objects = meraki_api.get_organization_policy_objects(api_key, organization_id) + policy_objects_groups = meraki_api.get_organization_policy_objects_groups(api_key, organization_id) + + # Create mappings for objects and groups + obj_mapping = {str(obj['id']): obj['name'] for obj in policy_objects} + group_mapping = {str(group['id']): group['name'] for group in policy_objects_groups} + + term_extra.clear_screen() + term_extra.print_ascii_art() + + if rules: + table = Table(show_header=True, header_style="green", box=SIMPLE) + priority_columns = ['policy', 'protocol', 'srcPort', 'srcCidr', 'destPort', 'destCidr'] + excluded_columns = ['syslogEnabled'] + other_columns = [key for key in rules[0].keys() if key not in priority_columns and key not in excluded_columns] + + for key in priority_columns + other_columns: + table.add_column(key.upper(), no_wrap=False) + + for rule in rules: + for cidr_field in ['srcCidr', 'destCidr']: + original_cidr = rule[cidr_field] + + if original_cidr.startswith("OBJ(") or original_cidr.startswith("GRP("): + obj_or_grp_id = original_cidr[4:-1] + rule[cidr_field] = obj_mapping.get(obj_or_grp_id, group_mapping.get(obj_or_grp_id, original_cidr)) + + row_data = [str(rule.get(key, "")) for key in priority_columns + other_columns] + policy = rule.get("policy", "").lower() + row_style = "green" if policy == "allow" else "red" if policy == "deny" else "" + styled_row_data = [Text(cell, style=row_style) for cell in row_data] + table.add_row(*styled_row_data) + + console = Console() + console.print(table) + else: + print(colored("No firewall rules found in the selected network.", "red")) + input(colored("\nPress Enter to return to the previous menu...", "green")) + + + + +# ================================================== +# PROCESS Data Inside Networks (MX Firewall Rules) +# ================================================== +def select_mx_network(api_key, organization_id): + selected_network = meraki_api.select_mx_network(api_key, organization_id) + if selected_network: + network_name = selected_network['name'] + network_id = selected_network['id'] + + display_firewall_rules(api_key, network_id, organization_id) + + downloads_path = str(Path.home() / "Downloads") + current_date = datetime.now().strftime("%Y-%m-%d") + meraki_dir = os.path.join(downloads_path, f"Cisco-Meraki-CLU-Export-{current_date}") + os.makedirs(meraki_dir, exist_ok=True) + + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + header = "MX Firewall Rules Menu" + options = [ + "List Firewall Rules", + "Download Firewall Rules CSV", + "Status (under dev)", + "Return to Main Menu" + ] + term_extra.print_header(header) + term_extra.print_menu(options) + + columns, _ = term_extra.get_terminal_size() + print("-" * columns) + + choice = input(colored("\nChoose a menu option [1-4]: ", "cyan")) + + if choice == '1': + display_firewall_rules(api_key, network_id) + + elif choice == '2': + firewall_rules = meraki_api.get_l3_firewall_rules(api_key, network_id) + + if firewall_rules: + meraki_api.export_firewall_rules_to_csv(firewall_rules, network_name, meraki_dir) + else: + print("No firewall rules to download.") + choice = input(colored("\nPress Enter to return to the precedent menu...", "green")) + + elif choice == '4': + break \ No newline at end of file diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/requirements.txt b/Source code/WINDOWS/akamura/ciscomerakiclu/requirements.txt new file mode 100644 index 0000000..564caa9 --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/requirements.txt @@ -0,0 +1,9 @@ +# Required dependencies to run Cisco Meraki CLU +tabulate +pathlib +datetime +termcolor +requests +rich +setuptools +cryptography \ No newline at end of file diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/settings/__init__.py b/Source code/WINDOWS/akamura/ciscomerakiclu/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/settings/db_creator.py b/Source code/WINDOWS/akamura/ciscomerakiclu/settings/db_creator.py new file mode 100644 index 0000000..2565aaa --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/settings/db_creator.py @@ -0,0 +1,89 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +import sqlite3 +from getpass import getpass +from termcolor import colored +from cryptography.fernet import Fernet +from base64 import urlsafe_b64encode + +# Utility function to generate a Fernet key from a password +def generate_fernet_key(password): + # This is a simple way to ensure the key size fits Fernet's requirements + return Fernet(urlsafe_b64encode(password.encode('utf-8').ljust(32)[:32])) + +# ================================================== +# CREATE the Database (Not encrypted by itself, encryption handled at data level) +# ================================================== +def create_cisco_meraki_clu_db(db_path): + try: + if not os.path.exists(os.path.dirname(db_path)): + os.makedirs(os.path.dirname(db_path)) + + # SQLite connection + conn = sqlite3.connect(db_path) + conn.execute("CREATE TABLE IF NOT EXISTS sensitive_data (id INTEGER PRIMARY KEY, data TEXT)") + print("Database and table created successfully.") + except Exception as e: + print(colored("\nFailed to create or access the database.\n", "red") + str(e)) + input("\nPress Enter to retry") + return False + finally: + if conn: + conn.close() + return True + +def database_exists(db_path): + return os.path.exists(db_path) + +# ================================================== +# PROMPT to Create Database +# ================================================== +def prompt_create_database(): + db_path = 'db/cisco_meraki_clu_db.db' + create_db = input("The program needs a database to store sensitive data like Cisco Meraki API key.\nDo you want to create the DB? [yes/no]: ").strip().lower() + if create_db == 'yes': + print(colored("\nRemember to save your database encryption key in a safe place!", "red")) + print(colored("You will need it to access the application!\n", "red")) + if create_cisco_meraki_clu_db(db_path): + print(colored("Database created successfully.", "green")) + return True + elif create_db == 'no': + print(colored("\nNo database created. Exiting the program.\n", "red")) + return False + else: + print(colored("\nInvalid input. Please try again.\n", "red")) + return prompt_create_database() # Recursively call until valid input is received + diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/settings/term_extra.py b/Source code/WINDOWS/akamura/ciscomerakiclu/settings/term_extra.py new file mode 100644 index 0000000..2c3ac23 --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/settings/term_extra.py @@ -0,0 +1,82 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +import shutil + + +# ================================================== +# CREATE the application layout +# ================================================== +def print_header(title): + columns, rows = get_terminal_size() + print("-" * columns) + +def print_menu(options): + columns, rows = get_terminal_size() + half = len(options) // 2 + for i in range(half): + left_option = f"{i+1}. {options[i]}" + right_option = f"{i+1+half}. {options[i+half]}" if i + half < len(options) else '' + print(f"{left_option:<{columns//2}}{right_option}") + +def print_footer(footer_text): + columns, _ = get_terminal_size() + print("-" * columns) + print("\n") + lines = footer_text.split('\n') + for line in lines: + print(line.ljust(columns)) + print("\n") + + +# ================================================== +# CLEAR the screen and present the main menu +# ================================================== +def clear_screen(): + os.system('cls') + +def print_ascii_art(): + ascii_art = """ + ____ _ __ __ _ _ ____ _ _ _ + / ___(_)___ ___ ___ | \/ | ___ _ __ __ _| | _(_) / ___| | | | | | +| | | / __|/ __/ _ \ | |\/| |/ _ \ '__/ _` | |/ / | | | | | | | | | +| |___| \__ \ (_| (_) | | | | | __/ | | (_| | <| | | |___| |__| |_| | + \____|_|___/\___\___/ |_| |_|\___|_| \__,_|_|\_\_| \____|_____\___/ +""" + print(ascii_art) + +def get_terminal_size(): + columns, rows = shutil.get_terminal_size() + return columns, rows \ No newline at end of file diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/setup.py b/Source code/WINDOWS/akamura/ciscomerakiclu/setup.py new file mode 100644 index 0000000..03d9fc0 --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/setup.py @@ -0,0 +1,61 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +from setuptools import setup, find_packages + + +# ================================================== +# SETUP the environment with needed libraries +# ================================================== +setup( + name='Cisco Meraki CLU', + version='1.3', + packages=find_packages(), + install_requires=[ + 'tabulate', + 'pathlib', + 'datetime', + 'termcolor', + 'requests', + 'cryptography', + 'rich', + 'setuptools' + ], + include_package_data=True, + entry_points={ + 'console_scripts': [ + 'ciscomerakiclu = ciscomerakiclu.main:main' + ] + } +) diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/utilities/__init__.py b/Source code/WINDOWS/akamura/ciscomerakiclu/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/utilities/menu.py b/Source code/WINDOWS/akamura/ciscomerakiclu/utilities/menu.py new file mode 100644 index 0000000..7dd36ba --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/utilities/menu.py @@ -0,0 +1,39 @@ +from consolemenu import ConsoleMenu +from consolemenu.items import MenuItem + + +def print_ascii_art(): + ascii_art = """ + ____ _ __ __ _ _ ____ _ _ _ + / ___(_)___ ___ ___ | \/ | ___ _ __ __ _| | _(_) / ___| | | | | | +| | | / __|/ __/ _ \ | |\/| |/ _ \ '__/ _` | |/ / | | | | | | | | | +| |___| \__ \ (_| (_) | | | | | __/ | | (_| | <| | | |___| |__| |_| | + \____|_|___/\___\___/ |_| |_|\___|_| \__,_|_|\_\_| \____|_____\___/ +""" + print(ascii_art) + + +# Define a function to do nothing (for header and footer simulation) +def dummy_function(): + pass + +# Create the menu +menu = ConsoleMenu("Main Menu", prologue_text="is an essential tool crafted for Network Administrators managing Cisco Meraki networks. ", + epilogue_text="Disclaimer\nThis utility is not an official Cisco Meraki product but is based on the official Cisco Meraki API.\nIt is intended to provide Network Administrators with an easy daily companion in the swiss army knife. \n☕️ Fuel me a coffee if you found it useful: https://www.paypal.com/paypalme/matiazanella/") + +# Add body items (actual menu items or functions) +body_item_1 = MenuItem("Security Appliance", menu) +body_item_2 = MenuItem("Switches and Access Points", menu) +body_item_3 = MenuItem("Sensors [Under Dev]", menu) +body_item_4 = MenuItem("Swiss Army Knife", menu) +body_item_5 = MenuItem("Meraki Platform Tools [Under Dev]", menu) +body_item_6 = MenuItem("Manage API Keys", menu) +menu.append_item(body_item_1) +menu.append_item(body_item_2) +menu.append_item(body_item_3) +menu.append_item(body_item_4) +menu.append_item(body_item_5) +menu.append_item(body_item_6) + +# Show the menu +menu.show() diff --git a/Source code/WINDOWS/akamura/ciscomerakiclu/utilities/submenu.py b/Source code/WINDOWS/akamura/ciscomerakiclu/utilities/submenu.py new file mode 100644 index 0000000..400d680 --- /dev/null +++ b/Source code/WINDOWS/akamura/ciscomerakiclu/utilities/submenu.py @@ -0,0 +1,168 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# IMPORT various libraries and modules +# ================================================== +import os +from pathlib import Path +from datetime import datetime +from termcolor import colored + + +# ================================================== +# IMPORT custom modules +# ================================================== +from modules.meraki import meraki_api +from modules.meraki import meraki_ms_mr +from modules.meraki import meraki_mx +from settings import term_extra + + +# ================================================== +# VISUALIZE submenus for Appliance, Switches and APs +# ================================================== +def select_organization(api_key): + selected_org = meraki_api.select_organization(api_key) + return selected_org + +def submenu_sw_and_ap(api_key): + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + header = "" + options = ["Select an Organization", "Return to Main Menu"] + term_extra.print_header(header) + term_extra.print_menu(options) + choice = input(colored("\nChoose a menu option [1-2]: ", "cyan")) + + if choice == '1': + selected_org = select_organization(api_key) + if selected_org: + term_extra.clear_screen() + term_extra.print_ascii_art() + term_extra.print_header(header) + print(colored(f"\nYou selected {selected_org['name']}.\n", "green")) + select_network(api_key, selected_org['id']) + elif choice == '2': + break + +def submenu_mx(api_key): + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + header = "" + options = ["Select an Organization", "Return to Main Menu"] + term_extra.print_header(header) + term_extra.print_menu(options) + choice = input(colored("\nChoose a menu option [1-2]: ", "cyan")) + + if choice == '1': + selected_org = select_organization(api_key) + if selected_org: + term_extra.clear_screen() + term_extra.print_ascii_art() + term_extra.print_header(header) + print(colored(f"\nYou selected {selected_org['name']}.\n", "green")) + meraki_mx.select_mx_network(api_key, selected_org['id']) + elif choice == '2': + break + + +# ================================================== +# DEFINE how to process data inside Networks +# ================================================== +def select_network(api_key, organization_id): + selected_network = meraki_api.select_network(api_key, organization_id) + if selected_network: + network_name = selected_network['name'] + network_id = selected_network['id'] + + downloads_path = str(Path.home() / "Downloads") + current_date = datetime.now().strftime("%Y-%m-%d") + meraki_dir = os.path.join(downloads_path, f"Cisco-Meraki-CLU-Export-{current_date}") + os.makedirs(meraki_dir, exist_ok=True) + + while True: + term_extra.clear_screen() + term_extra.print_ascii_art() + header = "Network Menu" + options = [ + "Get Switches", + "Get Access Points", + "Get Switch Ports", + "Get Devices Statuses", + "Download Switches CSV", + "Download Access Points CSV", + "Download Devices Statuses CSV (under dev)", + "Return to Main Menu" + ] + term_extra.print_header(header) + term_extra.print_menu(options) + + columns, _ = term_extra.get_terminal_size() + print("-" * columns) + + choice = input(colored("\nChoose a menu option [1-8]: ", "cyan")) + + if choice == '1': + meraki_ms_mr.display_devices(api_key, network_id, 'switches') + elif choice == '2': + meraki_ms_mr.display_devices(api_key, network_id, 'access_points') + elif choice == '3': + serial_number = input("\nEnter the switch serial number: ") + if serial_number: + print(f"Fetching switch ports for serial: {serial_number}") + meraki_ms_mr.display_switch_ports(api_key, serial_number) + else: + print("[red]Invalid input. Please enter a valid serial number.[/red]") + elif choice == '4': + meraki_ms_mr.display_organization_devices_statuses(api_key, organization_id, network_id) + elif choice == '5': + switches = meraki_api.get_meraki_switches(api_key, network_id) + if switches: + meraki_api.export_devices_to_csv(switches, network_name, 'switches', meraki_dir) + else: + print("No switches to download.") + choice = input(colored("\nPress Enter to return to the precedent menu...", "green")) + + elif choice == '6': + access_points = meraki_api.get_meraki_access_points(api_key, network_id) + if access_points: + meraki_api.export_devices_to_csv(access_points, network_name, 'access_points', meraki_dir) + else: + print("No access points to download.") + choice = input(colored("\nPress Enter to return to the precedent menu...", "green")) + + elif choice == '8': + break + else: + print("[red]No network selected or invalid organization ID.[/red]") \ No newline at end of file diff --git a/Source code/WINDOWS/setup.ps1 b/Source code/WINDOWS/setup.ps1 new file mode 100644 index 0000000..e2b89b5 --- /dev/null +++ b/Source code/WINDOWS/setup.ps1 @@ -0,0 +1,85 @@ +#************************************************************************** +# App: Cisco Meraki CLU * +# Version: 1.3 * +# Author: Matia Zanella * +# Description: Cisco Meraki CLU (Command Line Utility) is an essential * +# tool crafted for Network Administrators managing Meraki * +# Github: https://github.com/akamura/cisco-meraki-clu/ * +# * +# Icon Author: Cisco Systems, Inc. * +# Icon Author URL: https://meraki.cisco.com/ * +# * +# Copyright (C) 2024 Matia Zanella * +# https://www.matiazanella.com * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU General Public License as published by * +# the Free Software Foundation; either version 2 of the License, or * +# (at your option) any later version. * +# * +# This program is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU General Public License for more details. * +# * +# You should have received a copy of the GNU General Public License * +# along with this program; if not, write to the * +# Free Software Foundation, Inc., * +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +#************************************************************************** + + +# ================================================== +# Define System Paths and identify Desktop location +# ================================================== +$utilitiesFolder = "$env:USERPROFILE\Utilities" +$akamuraFolder = ".\akamura" +$WScriptShell = New-Object -ComObject WScript.Shell +$desktopPath = $WScriptShell.SpecialFolders("Desktop") + + +# ================================================== +# MOVE akamura folder and subfolders in Utilities +# ================================================== +if (Test-Path -Path $akamuraFolder) { + $installFolder = Join-Path -Path $utilitiesFolder -ChildPath "akamura" + + # Create "Utilities" folder if it doesn't exist + if (-not (Test-Path -Path $utilitiesFolder)) { + New-Item -Path $utilitiesFolder -ItemType Directory + } + + # Check if "akamura" already exists in "Utilities", prompt for action + if (Test-Path -Path $installFolder) { + Write-Host "The 'akamura' folder already exists in '$utilitiesFolder'." + } else { + try { + Move-Item -Path $akamuraFolder -Destination $utilitiesFolder -Force + } catch { + Write-Error "Error moving akamura folder: $_" + } + } +} + +$targetFolder = $installFolder + + +# ================================================== +# CREATE the Application shortcut on Desktop +# ================================================== +if (Test-Path -Path $targetFolder) { + $Shortcut = $WScriptShell.CreateShortcut("$desktopPath\Cisco Meraki CLU.lnk") + $Shortcut.TargetPath = "C:\Windows\System32\cmd.exe" + $Shortcut.Arguments = "/c python `"$targetFolder\ciscomerakiclu\main.py`"" + $Shortcut.IconLocation = "$targetFolder\ciscomerakiclu\icons\cisco-meraki-icon.ico" + $Shortcut.WorkingDirectory = "$targetFolder\ciscomerakiclu" + + try { + $Shortcut.Save() + } catch { + Write-Error "Unable to save shortcut: $_" + } + +} else { + Write-Host "The 'akamura' folder was not moved successfully. Shortcut creation is skipped." +} \ No newline at end of file diff --git a/usr/share/applications/ciscomerakiclu.desktop b/usr/share/applications/ciscomerakiclu.desktop deleted file mode 100644 index a97450a..0000000 --- a/usr/share/applications/ciscomerakiclu.desktop +++ /dev/null @@ -1,7 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Terminal=true -Exec=/opt/akamura/ciscomerakiclu/run -Name=Cisco Meraki CLU -Icon=/opt/akamura/ciscomerakiclu/icons/cisco-meraki-icon.png