├── LICENSE ├── README.md ├── nihilist.py ├── setup.py └── visuals ├── nihilist_card.png └── nihilist_demo.gif /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nihilist 2 | 3 | Cisco IOS configuration analyzer for finding misconfigurations and vulnerabilities 4 | 5 | ![](/visuals/nihilist_card.png) 6 | 7 | ``` 8 | Nihilist: Cisco IOS Security Inspector 9 | Author: Magama Bazarov, 10 | Alias: Caster 11 | Version: 1.0 12 | Codename: Gestalt 13 | ``` 14 | 15 | # Disclaimer 16 | 17 | **Nihilist** is a security auditing tool designed for security engineers to assess the configuration of their own Cisco devices. Unauthorized use of this tool may be illegal. 18 | 19 | Before use, make sure that you have permission to analyze device configurations. Use of this tool must comply with local laws and not violate the policies of the organizations that own the devices being tested. 20 | 21 | ## Important 22 | 23 | - **Nihilist** is not designed to hack into Cisco devices and does not contain vulnerability exploitation features; 24 | - This tool does not change the device configuration or perform destructive actions; 25 | - The author of the tool is not liable for incorrect or illegal use of the tool; 26 | - The tool works solely by reading the device configuration and does not make any changes. It does not require an account with maximum privileges (`privilege level 15`) to operate. It is sufficient to grant access only to execute show commands (read-only), which makes auditing as secure as possible; 27 | - **Nihilist** uses SSH-only remote connectivity. 28 | 29 | # Underlying Mechanism 30 | 31 | **Nihilist** uses [netmiko](https://github.com/ktbyers/netmiko) to remotely connect to Cisco IOS devices via SSH. It executes Cisco IOS system commands to extract configuration data and analyze it for potential vulnerabilities and security issues. 32 | 33 | The user connects to the device themselves using **Nihilist** by entering their credentials. The tool only executes commands to view the configuration and does not make any changes to the device settings. Thus, an account with read-only privileges is sufficient for Nihilist to work. 34 | 35 | Nihilist does not use any exploits, malicious payloads or brute-force attacks. All security analysis is based solely on examining the device configuration. 36 | 37 | # Nihilist Demo 38 | 39 | ![](/visuals/nihilist_demo.gif) 40 | 41 | > Here is a demo of a Nihilist doing a security analysis of a Cisco router 42 | 43 | # Security Checks 44 | 45 | Nihilist performs a comprehensive security analysis, covering IOS security, link layer security, routing protocols, and redundancy protocols. It also supports router and L2/L3 switch analysis. 46 | 47 | For more details and usage, check out the [dedicated page on the Wiki of this repository](https://github.com/casterbyte/Nihilist/wiki/Mechanism-of-the-tool) 48 | 49 | # How to Use 50 | 51 | To install the Nihilist: 52 | 53 | ```bash 54 | :~$ sudo apt install git python3-colorama python3-netmiko 55 | :~$ git clone https://github.com/casterbyte/Nihilist 56 | :~$ cd Nihilist 57 | :~/Nihilist$ sudo python3 setup.py install 58 | :~$ nihilist --help 59 | 60 | usage: nihilist.py [-h] --ip IP --username USERNAME --password PASSWORD [--port PORT] [--router] [--l2-switch] [--l3-switch] 61 | 62 | options: 63 | -h, --help show this help message and exit 64 | --ip IP Specify the IP address of the device 65 | --username USERNAME SSH Username 66 | --password PASSWORD SSH Password 67 | --port PORT SSH Port (default:22) 68 | --router Specify if the device is a router 69 | --l2-switch Specify if the device is a L2 switch 70 | --l3-switch Specify if the device is a L3 switch 71 | ``` 72 | 73 | ## Trigger Arguments (CLI Options) 74 | 75 | **Nihilist** supports as input parameters: 76 | 77 | - `--ip`: the user will need to specify the IP address of their device; 78 | - `--username`: the username for SSH connection to the Cisco device; 79 | - `--password`: the password for SSH connection to the Cisco device; 80 | - `--port`: SSH port number, by default the tool uses port 22; 81 | - `--router`: if the Cisco device is a router; 82 | - `--l2-switch`: if it's a Cisco L2 switch; 83 | - `--l3-switch`: if it's a Cisco L3 switch. 84 | 85 | For example, here's how to run a security analysis on a Cisco router: 86 | 87 | ```bash 88 | :~$ nihilist --ip 10.1.10.2 --username caster --password caster --port 2222 --router 89 | ``` 90 | 91 | > The data passed here as arguments are fictitious for illustrative purposes 92 | 93 | # Tested Devices 94 | 95 | When I developed Nihilist I tested it successfully on the following devices: 96 | 97 | - Cisco ISR4321/K9, IOS-XE Software Version: `16.09.02` 98 | - Cisco WS-C2960+24TC-L, Software Version: `15.2(2)E8` 99 | 100 | # Copyright 101 | 102 | Copyright (c) 2025 Magama Bazarov. This project is licensed under the Apache 2.0 License 103 | 104 | This project is not affiliated with or endorsed by Cisco Systems, Inc. 105 | 106 | # Outro 107 | 108 | Cisco equipment is very common all over the world and security is a major issue. 109 | With the release of this tool I just want to make the world a little better. Use it wisely and take care of the security of your infrastructure. 110 | 111 | When I wrote this tool, I was inspired by the works of Friedrich Nietzsche and the release of this tool is my tribute to his writings. 112 | 113 | E-mail for contact: magamabazarov@mailbox.org 114 | -------------------------------------------------------------------------------- /nihilist.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Magama Bazarov 2 | # Licensed under the Apache 2.0 License 3 | # This project is not affiliated with or endorsed by Cisco Systems, Inc. 4 | 5 | import argparse 6 | import re 7 | import datetime 8 | import sys 9 | from netmiko import ConnectHandler, NetmikoAuthenticationException, NetmikoTimeoutException 10 | from colorama import Fore, Style 11 | 12 | # This is banner 13 | def banner(): 14 | banner_text = r""" 15 | _______ .__.__ .__.__ .__ __ 16 | \ \ |__| |__ |__| | |__| _______/ |_ 17 | / | \| | | \| | | | |/ ___/\ __\ 18 | / | \ | Y \ | |_| |\___ \ | | 19 | \____|__ /__|___| /__|____/__/____ > |__| 20 | \/ \/ \/ 21 | """ 22 | banner_text = " " + banner_text.replace("\n", "\n ") 23 | print(banner_text) 24 | print(" " + Fore.YELLOW + "Nihilist: Cisco IOS Security Inspector" + Style.RESET_ALL) 25 | print(" " + Fore.YELLOW + "Author: " + Style.RESET_ALL + "Magama Bazarov, ") 26 | print(" " + Fore.YELLOW + "Alias: " + Style.RESET_ALL + "Caster") 27 | print(" " + Fore.YELLOW + "Version: " + Style.RESET_ALL + "1.0") 28 | print(" " + Fore.YELLOW + "Codename: " + Style.RESET_ALL + "Gestalt") 29 | print(" " + Fore.YELLOW + "How to Use: " + Style.RESET_ALL + "https://github.com/casterbyte/Nihilist") 30 | print(" " + Fore.YELLOW + "Detailed Documentation: " + Style.RESET_ALL + "https://github.com/casterbyte/Nihilist/wiki/Mechanism-of-the-tool\n") 31 | print(" " + Fore.MAGENTA + "❝He who fights with monsters should look to it that he himself does not become a monster❞") 32 | print(" " + Fore.MAGENTA + "— Friedrich Nietzsche, 1886\n" + Style.RESET_ALL) 33 | 34 | # Connect to the Cisco IOS 35 | def connect_to_device(ip, username, password, port, device_type): 36 | print(Fore.WHITE + f"[*] Running on Python {sys.version.split()[0]}" + Style.RESET_ALL) 37 | device = { 38 | "device_type": "cisco_ios", 39 | "host": ip, 40 | "username": username, 41 | "password": password, 42 | "port": port, 43 | "timeout": 10, 44 | } 45 | try: 46 | print(Fore.GREEN + f"[*] Connecting to {device_type} at {ip}:{port}..." + Style.RESET_ALL) 47 | connection = ConnectHandler(**device) 48 | print(Fore.WHITE + "[*] Connection successful!\n" + Style.RESET_ALL) 49 | return connection 50 | except NetmikoAuthenticationException: 51 | print(Fore.RED + "[-] Authentication failed! Check your credentials." + Style.RESET_ALL) 52 | exit(1) 53 | except NetmikoTimeoutException: 54 | print(Fore.RED + "[-] Connection timed out! Check device availability." + Style.RESET_ALL) 55 | exit(1) 56 | except Exception as e: 57 | print(Fore.RED + f"[-] Connection failed: {e}" + Style.RESET_ALL) 58 | exit(1) 59 | 60 | # Simple separator 61 | def print_separator(): 62 | print(Fore.WHITE + Style.BRIGHT + "=" * 50 + Style.RESET_ALL) 63 | 64 | # Display Uptime 65 | def check_device_uptime(connection): 66 | try: 67 | # Execute command to get device uptime 68 | output = connection.send_command("show version | include uptime") 69 | 70 | # Match the hostname and uptime from the output 71 | match = re.match(r'(\S+) uptime is (.+)', output) 72 | 73 | if match: 74 | hostname, uptime = match.groups() 75 | print(Fore.GREEN + "[*] Device " + Fore.WHITE + f"'{hostname}'" + Fore.GREEN + f" Uptime: {uptime}" + Style.RESET_ALL) 76 | else: 77 | print(Fore.YELLOW + "[!] Unable to parse device uptime." + Style.RESET_ALL) 78 | 79 | except Exception as e: 80 | # Handle any errors during command execution 81 | print(Fore.RED + f"[-] Failed to retrieve uptime: {e}" + Style.RESET_ALL) 82 | 83 | # Checking Configuration Size 84 | def checking_config_size(connection): 85 | try: 86 | # Retrieve the configuration size from running config 87 | config_size_output = connection.send_command("show running-config | include Current configuration").strip() 88 | 89 | # Extract the configuration size using regex 90 | match = re.search(r"Current configuration : (\d+) bytes", config_size_output) 91 | if match: 92 | config_size = int(match.group(1)) 93 | print(Fore.GREEN + "[*] Configuration size: " + Fore.WHITE + f"{config_size} bytes" + Style.RESET_ALL) 94 | else: 95 | print(Fore.RED + "[!] WARNING: Unable to determine configuration size." + Style.RESET_ALL) 96 | 97 | except Exception as e: 98 | # Handle errors during command execution 99 | print(Fore.RED + f"[-] Failed to retrieve configuration size: {e}" + Style.RESET_ALL) 100 | 101 | # PAD Status 102 | def checking_pad_service(connection): 103 | # Prints a visual separator for clarity 104 | print_separator() 105 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking PAD Service (X.25)..." + Style.RESET_ALL) 106 | 107 | try: 108 | # Retrieve the current PAD service configuration from running config 109 | pad_config = connection.send_command("show running-config | include service pad").strip() 110 | except Exception as e: 111 | # Handle errors if the command fails 112 | print(Fore.RED + f"[-] Failed to retrieve PAD configuration: {e}" + Style.RESET_ALL) 113 | return 114 | 115 | # Check if PAD service is explicitly disabled 116 | if 'no service pad' in pad_config: 117 | print(Fore.GREEN + "[OK] PAD service explicitly disabled ('no service pad')." + Style.RESET_ALL) 118 | 119 | # Check if PAD service is enabled, which is a potential security risk 120 | elif 'service pad' in pad_config: 121 | print(Fore.RED + "[!] WARNING: 'service pad' is enabled. Attackers could exploit PAD (X.25) for unauthorized access." + Style.RESET_ALL) 122 | 123 | # If no explicit setting is found, assume PAD is disabled by default 124 | else: 125 | print(Fore.GREEN + "[OK] PAD service is disabled by default." + Style.RESET_ALL) 126 | 127 | # Checking service password-encryption 128 | def checking_service_password_encryption(connection): 129 | # Prints a visual separator for clarity 130 | print_separator() 131 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking Password Protection Policy" + Style.RESET_ALL) 132 | 133 | try: 134 | # Retrieve the full running configuration 135 | config = connection.send_command("show running-config") 136 | except Exception as e: 137 | # Handle errors if the command fails 138 | print(Fore.RED + f"[-] Failed to retrieve configuration: {e}" + Style.RESET_ALL) 139 | return 140 | 141 | # Check if 'service password-encryption' is present and not explicitly disabled 142 | service_encryption_match = re.search(r"(?'." + Style.RESET_ALL) 328 | else: 329 | # Confirm that VLAN 1 is not used as the Native VLAN on any trunk ports 330 | print(Fore.GREEN + "[OK] No trunk ports are using VLAN 1 as the Native VLAN." + Style.RESET_ALL) 331 | 332 | # Checking CDP 333 | def checking_cdp(connection): 334 | # Prints a visual separator for clarity 335 | print_separator() 336 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking CDP Operation" + Style.RESET_ALL) 337 | 338 | try: 339 | # Enable privileged EXEC mode 340 | connection.enable() 341 | 342 | # Disable terminal paging for uninterrupted output 343 | connection.send_command("terminal length 0") 344 | 345 | # Retrieve CDP status for all interfaces 346 | cdp_output = connection.send_command("show cdp interface") 347 | except Exception as e: 348 | # Handle errors if the command fails 349 | print(Fore.RED + f"[-] Failed to retrieve CDP status: {e}" + Style.RESET_ALL) 350 | return 351 | 352 | # Regex pattern to extract all interface blocks 353 | pattern = re.compile( 354 | r'(?P\S+)\s+is\s+(?:up|down),\s+line\s+protocol\s+is\s+(?:up|down)\n' 355 | r'(?:\s+.*\n)*?', 356 | re.MULTILINE 357 | ) 358 | 359 | all_blocks = pattern.findall(cdp_output) 360 | cdp_enabled_interfaces = [] 361 | 362 | # Regex pattern to match CDP-enabled interfaces 363 | block_pattern = re.compile( 364 | r'(?P(?P\S+)\s+is\s+(?:up|down),\s+line\s+protocol\s+is\s+(?:up|down)\n' 365 | r'(?:\s+.*\n)*?)' 366 | r'(?=\S+\s+is\s+(?:up|down),|$)', 367 | re.MULTILINE 368 | ) 369 | 370 | # Iterate through all detected blocks and check if CDP is active 371 | blocks = block_pattern.finditer(cdp_output) 372 | for match_block in blocks: 373 | block_text = match_block.group('block') 374 | intf_name = match_block.group('intf') 375 | 376 | # If the block mentions CDP packet transmission, CDP is enabled 377 | if re.search(r'Sending CDP packets every \d+ seconds', block_text): 378 | cdp_enabled_interfaces.append(intf_name) 379 | 380 | if cdp_enabled_interfaces: 381 | # Warn if CDP is enabled on any interfaces 382 | print(Fore.YELLOW + "[!] WARNING: CDP is enabled on the following interfaces:" + Style.RESET_ALL) 383 | for interface in cdp_enabled_interfaces: 384 | print(Fore.YELLOW + f" - {interface}" + Style.RESET_ALL) 385 | 386 | # Highlight security risks associated with CDP 387 | print(Fore.YELLOW + "[!] CDP frames carry sensitive information about the equipment." + Style.RESET_ALL) 388 | print(Fore.WHITE + "[*] Keep track of where CDP is active." + Style.RESET_ALL) 389 | print(Fore.WHITE + "[*] When disabling CDP, be careful not to break VoIP." + Style.RESET_ALL) 390 | else: 391 | # Confirm that CDP is disabled on all interfaces 392 | print(Fore.GREEN + "[OK] CDP is disabled on all interfaces." + Style.RESET_ALL) 393 | 394 | # Checking VTY Lines 395 | def checking_vty_security(connection): 396 | # Prints a visual separator for clarity 397 | print_separator() 398 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking VTY Lines" + Style.RESET_ALL) 399 | 400 | try: 401 | # Retrieve VTY line configuration 402 | config = connection.send_command("show running-config | section line vty") 403 | 404 | # Retrieve full configuration to check global settings like HTTP server status 405 | global_config = connection.send_command("show running-config") 406 | except Exception as e: 407 | # Handle errors if the command fails 408 | print(Fore.RED + f"[-] Failed to retrieve VTY configuration: {e}" + Style.RESET_ALL) 409 | return 410 | 411 | insecure_methods = [] 412 | ssh_enabled = False 413 | access_class_found = False 414 | login_local_found = False 415 | 416 | # Split configuration into VTY blocks for analysis 417 | vty_blocks = re.split(r"line vty \d+ \d+", config) 418 | 419 | for vty_block in vty_blocks: 420 | if not vty_block.strip(): 421 | continue 422 | 423 | # Check for transport input settings (protocols allowed for remote access) 424 | if "transport input" in vty_block: 425 | if "telnet" in vty_block: 426 | insecure_methods.append("Telnet") 427 | if "rlogin" in vty_block: 428 | insecure_methods.append("RLogin") 429 | if "ssh" in vty_block: 430 | ssh_enabled = True 431 | 432 | # Check if access-class is applied (restricts remote access) 433 | if "access-class" in vty_block: 434 | access_class_found = True 435 | 436 | # Check if login local authentication is used (local username-based auth) 437 | if "login local" in vty_block: 438 | login_local_found = True 439 | 440 | # Warn if insecure transport methods are enabled (Telnet or RLogin) 441 | if insecure_methods: 442 | print(Fore.RED + "[!] WARNING: Insecure transport methods detected!" + Style.RESET_ALL) 443 | for method in set(insecure_methods): 444 | print(Fore.RED + f" - {method} is enabled on VTY lines. Consider disabling it (`transport input ssh`)." + Style.RESET_ALL) 445 | 446 | # Confirm if SSH is enabled (preferred secure access method) 447 | if ssh_enabled: 448 | print(Fore.GREEN + "[OK] SSH is enabled for secure remote access." + Style.RESET_ALL) 449 | 450 | # Inform if local authentication is used 451 | if login_local_found: 452 | print(Fore.WHITE + "[*] Local authentication (login local) is used for VTY access." + Style.RESET_ALL) 453 | 454 | # Warn if no access-class is applied (leaving remote access open) 455 | if not access_class_found: 456 | print(Fore.RED + "[!] WARNING: No 'access-class' applied to VTY lines!" + Style.RESET_ALL) 457 | print(Fore.RED + " - Your device is vulnerable to unauthorized remote access." + Style.RESET_ALL) 458 | print(Fore.RED + " - Consider applying an ACL using `access-class ACL_NAME in`." + Style.RESET_ALL) 459 | else: 460 | print(Fore.GREEN + "[OK] Access-class is applied, restricting remote access." + Style.RESET_ALL) 461 | 462 | # Web Service Activity: Checking if HTTP/HTTPS management is enabled 463 | http_server_disabled = "no ip http server" in global_config 464 | https_server_disabled = "no ip http secure-server" in global_config 465 | 466 | # Warn if web-based management interfaces are enabled (potential security risks) 467 | if not http_server_disabled or not https_server_disabled: 468 | print(Fore.YELLOW + "[!] WARNING: Web management interface (HTTP/HTTPS) is enabled!" + Style.RESET_ALL) 469 | print(Fore.YELLOW + " - Check your hardware for CVE-2023-20273 & CVE-2023-20198" + Style.RESET_ALL) 470 | print(Fore.YELLOW + " - If you're not using this as a control, you're better off turning it off" + Style.RESET_ALL) 471 | else: 472 | # Confirm if HTTP/HTTPS management is properly disabled 473 | print(Fore.GREEN + "[OK] HTTP/HTTPS management interface is disabled." + Style.RESET_ALL) 474 | 475 | # Checking AAA 476 | def checking_aaa(connection): 477 | # Prints a visual separator for clarity 478 | print_separator() 479 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking AAA Configuration" + Style.RESET_ALL) 480 | 481 | try: 482 | # Check if AAA is enabled by looking for 'aaa new-model' 483 | aaa_new_model = connection.send_command("show running-config | include aaa new-model").strip() 484 | 485 | # If AAA is not enabled, warn the user and stop further checks 486 | if not aaa_new_model or "no aaa new-model" in aaa_new_model: 487 | print(Fore.YELLOW + "[!] WARNING: AAA is not enabled (no 'aaa new-model' found). The device relies on local authentication only." + Style.RESET_ALL) 488 | return 489 | 490 | print(Fore.GREEN + "[OK] AAA is enabled on this device." + Style.RESET_ALL) 491 | 492 | # Dictionary to track authentication methods in use 493 | auth_methods = { 494 | "enable": [], 495 | "local": [], 496 | "none": [], 497 | "radius": [], 498 | "tacacs": [] 499 | } 500 | 501 | # Retrieve AAA authentication methods from running config 502 | method_lines = connection.send_command("show running-config | include aaa authentication login").strip().splitlines() 503 | 504 | # Parse authentication methods from configuration 505 | for line in method_lines: 506 | match = re.search(r'aaa authentication login (\S+) (.+)', line) 507 | if match: 508 | list_name, methods = match.groups() 509 | methods_list = methods.split() 510 | 511 | if "enable" in methods_list: 512 | auth_methods["enable"].append(list_name) 513 | if "local" in methods_list: 514 | auth_methods["local"].append(list_name) 515 | if "none" in methods_list: 516 | auth_methods["none"].append(list_name) 517 | if "group radius" in methods: 518 | auth_methods["radius"].append(list_name) 519 | if "group tacacs+" in methods: 520 | auth_methods["tacacs"].append(list_name) 521 | 522 | # Warn if 'none' is used in authentication (bypassing authentication) 523 | if auth_methods["none"]: 524 | for method in auth_methods["none"]: 525 | if method == "default": 526 | print(Fore.RED + "[!] CRITICAL: 'none' is used as the primary authentication method! Unauthorized access is possible!" + Style.RESET_ALL) 527 | else: 528 | print(Fore.YELLOW + f"[!] WARNING: 'none' is present in authentication list '{method}'. Consider removing it." + Style.RESET_ALL) 529 | 530 | # Warn if 'enable' password authentication is used (considered weak) 531 | if auth_methods["enable"]: 532 | print(Fore.YELLOW + "[!] WARNING: Authentication uses 'enable' password. Consider switching to more secure methods like RADIUS/TACACS+." + Style.RESET_ALL) 533 | 534 | # Confirm local authentication is in use 535 | if auth_methods["local"]: 536 | print(Fore.GREEN + "[OK] Local authentication is configured." + Style.RESET_ALL) 537 | 538 | # Confirm RADIUS authentication is enabled 539 | if auth_methods["radius"]: 540 | print(Fore.GREEN + "[OK] RADIUS authentication is enabled for login." + Style.RESET_ALL) 541 | 542 | # Confirm TACACS+ authentication is enabled 543 | if auth_methods["tacacs"]: 544 | print(Fore.GREEN + "[OK] TACACS+ authentication is enabled for login." + Style.RESET_ALL) 545 | 546 | # Warn if only local authentication is used without RADIUS/TACACS+ 547 | if not (auth_methods["radius"] or auth_methods["tacacs"]) and auth_methods["local"]: 548 | print(Fore.YELLOW + "[!] WARNING: Only local authentication is used. Ensure strong passwords for local users." + Style.RESET_ALL) 549 | 550 | # Check if AAA accounting is configured 551 | accounting_config = connection.send_command("show running-config | include aaa accounting").strip() 552 | 553 | # Warn if AAA accounting is missing (no logging of actions) 554 | if not accounting_config: 555 | print(Fore.YELLOW + "[!] WARNING: AAA accounting is not configured. Actions on the device are not logged." + Style.RESET_ALL) 556 | else: 557 | print(Fore.GREEN + "[OK] AAA accounting is enabled. Actions on the device are logged." + Style.RESET_ALL) 558 | 559 | except Exception as e: 560 | # Handle errors if the command execution fails 561 | print(Fore.RED + f"[-] Failed to retrieve AAA configuration: {e}" + Style.RESET_ALL) 562 | 563 | # Checking Sessions Limit 564 | def checking_session_limit(connection): 565 | # Prints a visual separator for clarity 566 | print_separator() 567 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking Sessions Limit" + Style.RESET_ALL) 568 | 569 | try: 570 | # Retrieve session limit configuration from running config 571 | session_limit_output = connection.send_command("show running-config | include session-limit").strip() 572 | 573 | # Warn if no session limit is explicitly configured 574 | if not session_limit_output: 575 | print(Fore.RED + "[!] WARNING: No session limit is set! Default (16) sessions are allowed." + Style.RESET_ALL) 576 | return 577 | 578 | # Extract the configured session limit value 579 | match = re.search(r'session-limit (\d+)', session_limit_output) 580 | if match: 581 | session_limit = int(match.group(1)) 582 | print(Fore.GREEN + f"[OK] Session limit is set to {session_limit} concurrent sessions." + Style.RESET_ALL) 583 | 584 | # Warn if the session limit is higher than the recommended value (default: 3) 585 | if session_limit > 3: 586 | print(Fore.YELLOW + f"[!] WARNING: The session limit is higher than recommended (3). Consider lowering it." + Style.RESET_ALL) 587 | print(Fore.YELLOW + f"[*] However, base it on your needs." + Style.RESET_ALL) 588 | else: 589 | # If session limit couldn't be parsed, display a warning 590 | print(Fore.RED + "[!] WARNING: Could not parse session limit value." + Style.RESET_ALL) 591 | 592 | except Exception as e: 593 | # Handle errors if the command execution fails 594 | print(Fore.RED + f"[-] Failed to retrieve session limit configuration: {e}" + Style.RESET_ALL) 595 | 596 | # Checking Login Block 597 | def checking_login_block_protection(connection): 598 | # Prints a visual separator for clarity 599 | print_separator() 600 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking Login Block" + Style.RESET_ALL) 601 | 602 | try: 603 | # Retrieve login block protection configuration 604 | login_block_output = connection.send_command("show running-config | include login block-for").strip() 605 | 606 | # Warn if brute-force protection is not configured 607 | if not login_block_output: 608 | print(Fore.RED + "[!] WARNING: No brute-force protection (login block) is configured." + Style.RESET_ALL) 609 | return 610 | 611 | # Extract login block settings from the configuration 612 | match = re.search(r'login block-for (\d+) attempts (\d+) within (\d+)', login_block_output) 613 | if match: 614 | block_time, attempts, within_time = match.groups() 615 | print(Fore.GREEN + f"[OK] Brute-force protection is enabled: {attempts} failed attempts within {within_time} sec → block for {block_time} sec." + Style.RESET_ALL) 616 | 617 | # Warn if the number of allowed failed attempts is too high 618 | if int(attempts) > 5: 619 | print(Fore.YELLOW + f"[!] WARNING: The failed attempts threshold ({attempts}) is too high. Recommended: 3." + Style.RESET_ALL) 620 | 621 | # Warn if the block time is too short for effective protection 622 | if int(block_time) < 30: 623 | print(Fore.YELLOW + f"[!] WARNING: The block time ({block_time} sec) is too short. Recommended: 60 sec or more." + Style.RESET_ALL) 624 | else: 625 | # If parsing fails, notify the user 626 | print(Fore.RED + "[!] WARNING: Could not parse brute-force protection configuration." + Style.RESET_ALL) 627 | 628 | except Exception as e: 629 | # Handle errors if the command execution fails 630 | print(Fore.RED + f"[-] Failed to retrieve login block configuration: {e}" + Style.RESET_ALL) 631 | 632 | # Checking SSH Security Settings 633 | def checking_ssh_security(connection): 634 | # Prints a visual separator for clarity 635 | print_separator() 636 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking SSH Security Settings" + Style.RESET_ALL) 637 | 638 | try: 639 | # Retrieve SSH-related configuration lines 640 | ssh_config = connection.send_command("show running-config | include ^ip ssh").strip().splitlines() 641 | 642 | # Default settings (Cisco default values) 643 | ssh_version = "Compatibility (1 & 2)" 644 | auth_retries = 3 # Default number of authentication retries 645 | timeout = 120 # Default session timeout in seconds 646 | maxstartups = 10 # Default max simultaneous SSH sessions 647 | 648 | # Parse SSH configuration for specific settings 649 | for line in ssh_config: 650 | if "ip ssh version" in line: 651 | ssh_version = line.split()[-1] # Extracts SSH version 652 | elif "ip ssh authentication-retries" in line: 653 | auth_retries = int(line.split()[-1]) # Extracts authentication retry count 654 | elif "ip ssh time-out" in line: 655 | timeout = int(line.split()[-1]) # Extracts session timeout value 656 | elif "ip ssh maxstartups" in line: 657 | maxstartups = int(line.split()[-1]) # Extracts max startup sessions value 658 | 659 | # Check SSH version (should be explicitly set to version 2) 660 | if ssh_version != "2": 661 | print(Fore.RED + "[!] WARNING: SSH version is not explicitly set to 2. Set with 'ip ssh version 2'." + Style.RESET_ALL) 662 | else: 663 | print(Fore.GREEN + "[OK] SSH version 2 is explicitly configured." + Style.RESET_ALL) 664 | 665 | # Check SSH authentication retry limit (should not be too high) 666 | if auth_retries > 3: 667 | print(Fore.YELLOW + f"[!] NOTICE: SSH authentication-retries ({auth_retries}) is slightly high. Recommended: ≤ 3." + Style.RESET_ALL) 668 | elif auth_retries == 3: 669 | print(Fore.GREEN + f"[OK] SSH authentication-retries ({auth_retries}) is secure." + Style.RESET_ALL) 670 | else: 671 | print(Fore.GREEN + f"[OK] SSH authentication-retries ({auth_retries}) is optimally low." + Style.RESET_ALL) 672 | 673 | # Check SSH session timeout (should be limited for security) 674 | if timeout > 120: 675 | print(Fore.RED + f"[!] WARNING: SSH timeout ({timeout}s) is too high. Recommended: ≤ 90s, ideally ≤ 60s." + Style.RESET_ALL) 676 | elif 90 < timeout <= 120: 677 | print(Fore.YELLOW + f"[!] NOTICE: SSH timeout ({timeout}s) is moderate. Consider ≤ 90s for better security." + Style.RESET_ALL) 678 | else: 679 | print(Fore.GREEN + f"[OK] SSH timeout ({timeout}s) is optimal." + Style.RESET_ALL) 680 | 681 | # Check max simultaneous SSH sessions allowed (should be restricted) 682 | if maxstartups > 4: 683 | print(Fore.RED + f"[!] WARNING: SSH maxstartups ({maxstartups}) is too high. Recommended: ≤ 4." + Style.RESET_ALL) 684 | elif maxstartups == 4: 685 | print(Fore.YELLOW + f"[!] NOTICE: SSH maxstartups ({maxstartups}) is reasonable, but ≤ 3 is preferred." + Style.RESET_ALL) 686 | else: 687 | print(Fore.GREEN + f"[OK] SSH maxstartups ({maxstartups}) is secure." + Style.RESET_ALL) 688 | 689 | except Exception as e: 690 | # Handle errors if the command execution fails 691 | print(Fore.RED + f"[-] Failed to retrieve SSH configuration: {e}" + Style.RESET_ALL) 692 | 693 | # Checking LLDP 694 | def checking_lldp(connection): 695 | # Prints a visual separator for clarity 696 | print_separator() 697 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking LLDP Operation" + Style.RESET_ALL) 698 | 699 | try: 700 | # Retrieve LLDP status for all interfaces 701 | lldp_output = connection.send_command("show lldp interface") 702 | except Exception as e: 703 | # Handle errors if the command fails 704 | print(Fore.RED + f"[-] Failed to retrieve LLDP status: {e}" + Style.RESET_ALL) 705 | return 706 | 707 | # Extract interfaces where LLDP is enabled for both transmission (Tx) and reception (Rx) 708 | lldp_enabled_interfaces = re.findall(r'(\S+):\n\s+Tx: enabled\n\s+Rx: enabled', lldp_output) 709 | 710 | if lldp_enabled_interfaces: 711 | # Warn if LLDP is enabled on any interfaces 712 | print(Fore.YELLOW + "[!] WARNING: LLDP is enabled on the following interfaces:" + Style.RESET_ALL) 713 | for interface in lldp_enabled_interfaces: 714 | print(Fore.YELLOW + f" - {Fore.YELLOW}{interface}{Style.RESET_ALL}") 715 | 716 | # Highlight security risks associated with LLDP 717 | print(Fore.YELLOW + "[!] LLDP frames carry sensitive information about the equipment." + Style.RESET_ALL) 718 | print(Fore.WHITE + "[*] Keep track of where LLDP is active." + Style.RESET_ALL) 719 | print(Fore.WHITE + "[*] When disabling LLDP, be careful not to break VoIP." + Style.RESET_ALL) 720 | else: 721 | # Confirm that LLDP is disabled on all interfaces 722 | print(Fore.GREEN + "[OK] LLDP is disabled on all interfaces." + Style.RESET_ALL) 723 | 724 | # Checking Default Usernames 725 | def checking_default_usernames(connection): 726 | # Prints a visual separator for clarity 727 | print_separator() 728 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking Default Usernames" + Style.RESET_ALL) 729 | 730 | try: 731 | # Retrieve all configured usernames from the running configuration 732 | config = connection.send_command("show running-config | sec username") 733 | except Exception as e: 734 | # Handle errors if the command execution fails 735 | print(Fore.RED + f"[-] Failed to retrieve usernames: {e}" + Style.RESET_ALL) 736 | return 737 | 738 | # List of commonly used default usernames that should be avoided 739 | default_usernames = {"user", "test", "cisco", "ciscoadmin", "root", "ciscoios", "c1sc0", "administrator", "admin"} 740 | 741 | # Extract all usernames from the configuration 742 | found_users = re.findall(r'username\s+(\S+)', config) 743 | 744 | # Identify usernames that match known default usernames 745 | flagged_users = [user for user in found_users if user.lower() in default_usernames] 746 | 747 | if flagged_users: 748 | # Warn if any default usernames are found 749 | print(Fore.YELLOW + "[!] WARNING: Default usernames detected!" + Style.RESET_ALL) 750 | for user in flagged_users: 751 | print(Fore.YELLOW + f" - {Fore.WHITE}{user}{Style.RESET_ALL}") 752 | 753 | # Highlight the security risk of using common usernames 754 | print(Fore.YELLOW + "[!] Using default usernames increases the risk of brute-force attacks. Change them to something more unique." + Style.RESET_ALL) 755 | else: 756 | # Confirm that no default usernames are present 757 | print(Fore.GREEN + "[OK] No default usernames found." + Style.RESET_ALL) 758 | 759 | # Checking HSRP 760 | def checking_hsrp(connection): 761 | # Prints a visual separator for clarity 762 | print_separator() 763 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking HSRP Operation" + Style.RESET_ALL) 764 | 765 | try: 766 | # Retrieve HSRP configuration from the running configuration 767 | hsrp_config = connection.send_command("show running-config | section standby").strip() 768 | 769 | # If no HSRP configuration is found, assume the feature is not in use 770 | if not hsrp_config: 771 | print(Fore.GREEN + "[OK] No HSRP configuration found on this device." + Style.RESET_ALL) 772 | return 773 | 774 | # Retrieve HSRP active status information 775 | hsrp_status = connection.send_command("show standby brief") 776 | except Exception as e: 777 | # Handle errors if the command execution fails 778 | print(Fore.RED + f"[-] Failed to retrieve HSRP configuration: {e}" + Style.RESET_ALL) 779 | return 780 | 781 | # Extract all unique HSRP group numbers from the configuration 782 | hsrp_groups = list(set(re.findall(r'standby\s+(\d+)', hsrp_config))) 783 | 784 | priority_issues = [] # Stores HSRP groups with low priority 785 | no_auth = [] # Stores HSRP groups without authentication 786 | md5_auth = [] # Stores HSRP groups using MD5 authentication 787 | plain_auth = [] # Stores HSRP groups using plaintext authentication 788 | 789 | for group in hsrp_groups: 790 | # Extract HSRP priority for each group 791 | pm = re.search(rf'standby {group} priority (\d+)', hsrp_config) 792 | priority = int(pm.group(1)) if pm else 100 # Default priority is 100 if not set 793 | 794 | # Check if the group is active and its priority is below 255 (not ideal) 795 | ac = re.search(rf'^\S+\s+{group}\s+(\d+)\s+\S*\s+Active', hsrp_status, re.MULTILINE) 796 | if ac and priority < 255: 797 | priority_issues.append( 798 | f" - Group {group}: priority is {priority}. {Fore.RED}Should be 255 for Active role{Style.RESET_ALL}" 799 | ) 800 | 801 | # Extract HSRP authentication settings 802 | auth_line = re.search(rf'^.*standby {group} authentication (.*)$', hsrp_config, re.MULTILINE) 803 | if auth_line: 804 | if 'md5' in auth_line.group(1).lower(): 805 | md5_auth.append(f" - Group {group}") 806 | else: 807 | plain_auth.append(f" - Group {group}") 808 | else: 809 | no_auth.append(f" - Group {group}") 810 | 811 | issues_found = (priority_issues or no_auth or plain_auth) 812 | 813 | if issues_found: 814 | # Warn about HSRP security risks and possible MITM attacks 815 | print(Fore.YELLOW + "[!] WARNING: HSRP security issues detected. Possible MITM risk." + Style.RESET_ALL) 816 | 817 | if priority_issues: 818 | print(Fore.YELLOW + "[!] HSRP groups with priority issues:" + Style.RESET_ALL) 819 | for issue in priority_issues: 820 | print(issue) 821 | 822 | if no_auth: 823 | print(Fore.YELLOW + "[!] HSRP groups without any authentication:" + Style.RESET_ALL) 824 | for group in no_auth: 825 | print(Fore.YELLOW + group + Style.RESET_ALL) 826 | 827 | if plain_auth: 828 | print(Fore.RED + "[!] HSRP groups using plaintext authentication:" + Style.RESET_ALL) 829 | for group in plain_auth: 830 | print(Fore.RED + group + Style.RESET_ALL) 831 | 832 | # Confirm if MD5 authentication is in use for HSRP groups 833 | if md5_auth: 834 | print(Fore.GREEN + "[OK] HSRP MD5 authentication is enabled on the following groups:" + Style.RESET_ALL) 835 | for group in md5_auth: 836 | print(Fore.GREEN + group + Style.RESET_ALL) 837 | 838 | # Inform that HSRP priority values may vary depending on infrastructure design 839 | print(Fore.WHITE + "[*] HSRP priorities can be configured differently in different infrastructures, you may even get MHSRP." + Style.RESET_ALL) 840 | 841 | if not issues_found: 842 | # Confirm that no security vulnerabilities were detected in HSRP configuration 843 | print(Fore.GREEN + "[OK] No security issues found with HSRP configuration." + Style.RESET_ALL) 844 | 845 | # Checking VRRP 846 | def checking_vrrp(connection): 847 | # Prints a visual separator for clarity 848 | print_separator() 849 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking VRRP Operation" + Style.RESET_ALL) 850 | 851 | try: 852 | # Retrieve VRRP configuration from the running configuration 853 | vrrp_config = connection.send_command("show running-config | section vrrp").strip() 854 | 855 | # If no VRRP configuration is found, assume the feature is not in use 856 | if not vrrp_config: 857 | print(Fore.GREEN + "[OK] No VRRP configuration found on this device." + Style.RESET_ALL) 858 | return 859 | 860 | except Exception as e: 861 | # Handle errors if the command execution fails 862 | print(Fore.RED + f"[-] Failed to retrieve VRRP configuration: {e}" + Style.RESET_ALL) 863 | return 864 | 865 | # Extract all unique VRRP group numbers from the configuration 866 | vrrp_instances = re.findall(r'vrrp\s+(\d+)', vrrp_config) 867 | vrrp_instances = list(set(vrrp_instances)) 868 | 869 | priority_warnings = [] # Stores VRRP groups with low priority 870 | auth_issues = [] # Stores VRRP groups without authentication 871 | auth_md5 = [] # Stores VRRP groups using MD5 authentication 872 | weak_auth = [] # Stores VRRP groups using plaintext authentication 873 | 874 | for group in vrrp_instances: 875 | # Extract VRRP priority for each group 876 | priority_match = re.search(rf'vrrp {group} priority (\d+)', vrrp_config) 877 | priority = int(priority_match.group(1)) if priority_match else 100 # Default priority is 100 if not set 878 | 879 | priority_warnings.append(f" - Group {group}: priority is {priority} (Max possible is 254. You can protect yourself from a MITM attack with authentication)") 880 | 881 | # Extract VRRP authentication settings 882 | auth_text_match = re.search(rf'vrrp {group} authentication text ', vrrp_config) 883 | auth_md5_keychain_match = re.search(rf'vrrp {group} authentication md5 key-chain ', vrrp_config) 884 | auth_md5_keystring_match = re.search(rf'vrrp {group} authentication md5 key-string ', vrrp_config) 885 | 886 | if auth_md5_keychain_match or auth_md5_keystring_match: 887 | auth_md5.append(f" - Group {group}") 888 | elif auth_text_match: 889 | weak_auth.append(f" - Group {group}") 890 | else: 891 | auth_issues.append(f" - Group {group}") 892 | 893 | if priority_warnings or auth_issues or weak_auth: 894 | # Warn about VRRP security risks and possible MITM attacks 895 | print(Fore.YELLOW + "[!] WARNING: VRRP security issues detected. Possible MITM risk." + Style.RESET_ALL) 896 | 897 | if priority_warnings: 898 | print(Fore.YELLOW + "[!] VRRP groups and their priorities:" + Style.RESET_ALL) 899 | for issue in priority_warnings: 900 | print(Fore.YELLOW + issue + Style.RESET_ALL) 901 | 902 | if weak_auth: 903 | print(Fore.RED + "[!] VRRP groups using plaintext authentication:" + Style.RESET_ALL) 904 | for issue in weak_auth: 905 | print(Fore.RED + issue + Style.RESET_ALL) 906 | 907 | if auth_issues: 908 | print(Fore.RED + "[!] VRRP groups without authentication:" + Style.RESET_ALL) 909 | for issue in auth_issues: 910 | print(Fore.RED + issue + Style.RESET_ALL) 911 | 912 | # Confirm if MD5 authentication is in use for VRRP groups 913 | if auth_md5: 914 | print(Fore.GREEN + "[OK] VRRP MD5 authentication is enabled on the following groups:" + Style.RESET_ALL) 915 | for success in auth_md5: 916 | print(Fore.GREEN + success + Style.RESET_ALL) 917 | 918 | # Inform that VRRP priority values may vary depending on infrastructure design 919 | print(Fore.WHITE + "[*] VRRP priorities can be configured differently in different infrastructures, you may even get MVRRP." + Style.RESET_ALL) 920 | 921 | if not priority_warnings and not auth_issues and not weak_auth: 922 | # Confirm that no security vulnerabilities were detected in VRRP configuration 923 | print(Fore.GREEN + "[OK] No security issues found with VRRP configuration." + Style.RESET_ALL) 924 | 925 | # Checking GLBP 926 | def checking_glbp(connection): 927 | # Prints a visual separator for clarity 928 | print_separator() 929 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking GLBP Operation" + Style.RESET_ALL) 930 | 931 | try: 932 | # Retrieve GLBP configuration from the running configuration 933 | glbp_config = connection.send_command("show running-config | section glbp").strip() 934 | 935 | # If no GLBP configuration is found, assume the feature is not in use 936 | if not glbp_config: 937 | print(Fore.GREEN + "[OK] No GLBP configuration found on this device." + Style.RESET_ALL) 938 | return 939 | 940 | # Retrieve GLBP brief status information 941 | glbp_brief = connection.send_command("show glbp brief") 942 | except Exception as e: 943 | # Handle errors if the command execution fails 944 | print(Fore.RED + f"[-] Failed to retrieve GLBP configuration: {e}" + Style.RESET_ALL) 945 | return 946 | 947 | # Extract all unique GLBP group numbers from the configuration 948 | glbp_groups = list(set(re.findall(r'glbp\s+(\d+)\s', glbp_config))) 949 | 950 | priority_issues = [] # Stores GLBP groups with low AVG priority 951 | no_auth = [] # Stores GLBP groups without authentication 952 | md5_auth = [] # Stores GLBP groups using MD5 authentication 953 | plain_auth = [] # Stores GLBP groups using plaintext authentication 954 | 955 | # Regular expression pattern for parsing GLBP brief output 956 | pattern = re.compile(r'^(?P\S+)\s+(?P\d+)\s+(?P-|\d+)\s+(?P\d+|-)\s+(?P\S+)', re.MULTILINE) 957 | brief_matches = pattern.findall(glbp_brief) 958 | 959 | # Dictionary to store actual roles and priorities of GLBP groups 960 | actual_roles = {} 961 | 962 | for intf, grp, fwd, pri, state in brief_matches: 963 | if grp not in actual_roles: 964 | actual_roles[grp] = [] 965 | is_avg = (fwd == '-') # Check if the group acts as the AVG (Active Virtual Gateway) 966 | 967 | real_pri = int(pri) if pri.isdigit() else 100 # Default priority is 100 if not explicitly set 968 | actual_roles[grp].append({ 969 | "interface": intf, 970 | "fwd": fwd, 971 | "priority": real_pri, 972 | "state": state.lower(), 973 | "is_avg": is_avg 974 | }) 975 | 976 | for group in glbp_groups: 977 | # Extract GLBP priority for each group 978 | match_priority = re.search(rf'glbp {group} priority (\d+)', glbp_config) 979 | config_prio = int(match_priority.group(1)) if match_priority else 100 980 | 981 | # Extract authentication settings 982 | auth_line = re.search(rf'^.*glbp {group} authentication (\S+)\s+(.*)$', glbp_config, re.MULTILINE) 983 | if auth_line: 984 | auth_type = auth_line.group(1).lower() 985 | if auth_type == "md5": 986 | md5_auth.append(f" - Group {group}") 987 | elif auth_type == "text": 988 | plain_auth.append(f" - Group {group}") 989 | else: 990 | no_auth.append(f" - Group {group}") 991 | else: 992 | no_auth.append(f" - Group {group}") 993 | 994 | # Analyze GLBP roles and priority settings 995 | group_role_info = actual_roles.get(group, []) 996 | for role in group_role_info: 997 | if role["is_avg"] and role["state"] == "active": 998 | if config_prio < 255: 999 | priority_issues.append( 1000 | f" - Group {group}: priority is {config_prio}. {Fore.RED}Should be 255 for the AVG{Style.RESET_ALL}" 1001 | ) 1002 | 1003 | issues_found = (priority_issues or no_auth or plain_auth) 1004 | 1005 | if issues_found: 1006 | # Warn about GLBP security risks and possible MITM attacks 1007 | print(Fore.YELLOW + "[!] WARNING: GLBP security issues detected. Possible MITM risk." + Style.RESET_ALL) 1008 | 1009 | if priority_issues: 1010 | print(Fore.YELLOW + "[!] GLBP AVG with priority <255:" + Style.RESET_ALL) 1011 | for issue in priority_issues: 1012 | print(issue) 1013 | 1014 | if plain_auth: 1015 | print(Fore.RED + "[!] GLBP groups using plaintext authentication:" + Style.RESET_ALL) 1016 | for grp in plain_auth: 1017 | print(Fore.RED + grp + Style.RESET_ALL) 1018 | 1019 | if no_auth: 1020 | print(Fore.RED + "[!] GLBP groups without authentication:" + Style.RESET_ALL) 1021 | for grp in no_auth: 1022 | print(Fore.RED + grp + Style.RESET_ALL) 1023 | 1024 | # Confirm if MD5 authentication is in use for GLBP groups 1025 | if md5_auth: 1026 | print(Fore.GREEN + "[OK] GLBP MD5 authentication is enabled on the following groups:" + Style.RESET_ALL) 1027 | for g in md5_auth: 1028 | print(Fore.GREEN + g + Style.RESET_ALL) 1029 | 1030 | # Inform that GLBP priority values may vary depending on infrastructure design 1031 | print(Fore.WHITE + Style.BRIGHT + "[*] GLBP settings in the context of prioritization can vary across infrastructures. Keep in mind." + Style.RESET_ALL) 1032 | 1033 | if not issues_found: 1034 | # Confirm that no security vulnerabilities were detected in GLBP configuration 1035 | print(Fore.GREEN + "[OK] No security issues found with GLBP configuration." + Style.RESET_ALL) 1036 | 1037 | # Checking SNMP 1038 | def checking_snmp(connection): 1039 | # Prints a visual separator for clarity 1040 | print_separator() 1041 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking SNMP Operation" + Style.RESET_ALL) 1042 | 1043 | try: 1044 | # Retrieve SNMP configuration from the running configuration 1045 | snmp_config = connection.send_command("show running-config | section snmp").strip() 1046 | 1047 | # If no SNMP configuration is found, assume SNMP is not in use 1048 | if not snmp_config: 1049 | print(Fore.GREEN + "[OK] No SNMP configuration found on this device." + Style.RESET_ALL) 1050 | return 1051 | 1052 | except Exception as e: 1053 | # Handle errors if the command execution fails 1054 | print(Fore.RED + f"[-] Failed to retrieve SNMP configuration: {e}" + Style.RESET_ALL) 1055 | return 1056 | 1057 | # List of commonly used weak SNMP community strings 1058 | weak_snmp_strings = ["public", "private", "cisco", "admin", "root", "ciscoadmin", "c1sc0", "ciscorouter", "ciscoios"] 1059 | 1060 | # Extract SNMP community strings and their access levels (RO = Read-Only, RW = Read-Write) 1061 | snmp_entries = re.findall(r'snmp-server community (\S+) (RO|RW)', snmp_config) 1062 | 1063 | rw_issues = [] # Stores SNMP communities with RW (Read-Write) access 1064 | weak_issues = [] # Stores SNMP communities with weak names 1065 | ro_entries = [] # Stores SNMP communities with RO (Read-Only) access 1066 | 1067 | for community, access in snmp_entries: 1068 | # Flag communities with RW access as a high security risk 1069 | if access == "RW": 1070 | rw_issues.append(f" - {community}: RW access (Dangerous! If an attacker obtains this, they may download the router's configuration via TFTP)") 1071 | 1072 | # Flag communities with weak, easily guessable names 1073 | if community.lower() in weak_snmp_strings: 1074 | weak_issues.append(f" - {community}: Weak SNMP community detected") 1075 | 1076 | # Log communities with RO access (not as risky, but still a potential security concern) 1077 | if access == "RO": 1078 | ro_entries.append(f" - {community}: RO access (Less risky, but still should be restricted)") 1079 | 1080 | if rw_issues or weak_issues: 1081 | # Warn about SNMP security risks, including weak or RW-enabled community strings 1082 | print(Fore.YELLOW + "[!] WARNING: SNMP security issues detected!" + Style.RESET_ALL) 1083 | 1084 | if rw_issues: 1085 | print(Fore.YELLOW + "[!] SNMP RW communities found:" + Style.RESET_ALL) 1086 | for issue in rw_issues: 1087 | print(Fore.RED + issue + Style.RESET_ALL) 1088 | 1089 | if weak_issues: 1090 | print(Fore.YELLOW + "[!] Weak SNMP community strings detected!" + Style.RESET_ALL) 1091 | for issue in weak_issues: 1092 | print(Fore.YELLOW + issue + Style.RESET_ALL) 1093 | 1094 | # Display SNMP RO communities, which pose a lower but still notable security risk 1095 | if ro_entries: 1096 | print(Fore.BLUE + "[*] SNMP RO communities found:" + Style.RESET_ALL) 1097 | for entry in ro_entries: 1098 | print(Fore.BLUE + entry + Style.RESET_ALL) 1099 | 1100 | if not rw_issues and not weak_issues: 1101 | # Confirm that no critical SNMP security issues were detected 1102 | print(Fore.GREEN + "[OK] No SNMP security issues found." + Style.RESET_ALL) 1103 | 1104 | # Checking DHCP Snooping 1105 | def checking_dhcp_snooping(connection): 1106 | # Prints a visual separator for clarity 1107 | print_separator() 1108 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking DHCP Snooping Operation" + Style.RESET_ALL) 1109 | 1110 | try: 1111 | # Retrieve DHCP Snooping configuration from the running configuration 1112 | dhcp_snooping_config = connection.send_command("show running-config | section dhcp snooping").strip() 1113 | 1114 | # If no DHCP Snooping configuration is found, assume the feature is not enabled 1115 | if not dhcp_snooping_config: 1116 | print(Fore.RED + "[!] WARNING: DHCP Snooping is NOT enabled on this device!" + Style.RESET_ALL) 1117 | return 1118 | 1119 | except Exception as e: 1120 | # Handle errors if the command execution fails 1121 | print(Fore.RED + f"[-] Failed to retrieve DHCP Snooping configuration: {e}" + Style.RESET_ALL) 1122 | return 1123 | 1124 | # Extract VLANs where DHCP Snooping is enabled 1125 | snooping_vlans = re.findall(r'ip dhcp snooping vlan (\S+)', dhcp_snooping_config) 1126 | 1127 | # Extract trusted DHCP Snooping ports 1128 | snooping_trust_ports = re.findall(r'ip dhcp snooping trust', dhcp_snooping_config) 1129 | 1130 | # Check if Option 82 (DHCP Snooping Information Option) is disabled 1131 | option_82_disabled = "no ip dhcp snooping information option" in dhcp_snooping_config 1132 | 1133 | # Check if a DHCP Snooping binding database is configured 1134 | snooping_database = re.search(r'ip dhcp snooping database\s+(flash:|ftp:|https:|rcp:|scp:|tftp:)/\S+', dhcp_snooping_config) 1135 | 1136 | # Check if a write-delay is configured for DHCP Snooping database updates 1137 | write_delay_match = re.search(r'ip dhcp snooping database write-delay (\d+)', dhcp_snooping_config) 1138 | 1139 | # Check if DHCP Snooping rate limiting is enabled on any ports 1140 | rate_limit = re.findall(r'ip dhcp snooping limit rate (\d+)', dhcp_snooping_config) 1141 | 1142 | issues = [] # Stores critical DHCP Snooping misconfigurations 1143 | notices = [] # Stores best practice recommendations 1144 | 1145 | if not snooping_vlans: 1146 | # Warn if DHCP Snooping is enabled but no VLANs are configured 1147 | issues.append("[!] DHCP Snooping is enabled, but no VLANs are configured!") 1148 | else: 1149 | print(Fore.GREEN + f"[OK] DHCP Snooping is enabled for VLANs: {', '.join(snooping_vlans)}" + Style.RESET_ALL) 1150 | 1151 | if not snooping_trust_ports: 1152 | # Warn if no trusted DHCP Snooping ports are defined 1153 | issues.append("[!] No trusted ports configured! DHCP replies might be blocked.") 1154 | else: 1155 | print(Fore.YELLOW + f"[*] DHCP Snooping trust is configured on {len(snooping_trust_ports)} ports." + Style.RESET_ALL) 1156 | 1157 | if not option_82_disabled: 1158 | # Notify the user that Option 82 is enabled (not necessarily a security issue) 1159 | issues.append("[*] DHCP Snooping Information Option (Option 82) is enabled. I'm just keeping you posted.") 1160 | else: 1161 | print(Fore.GREEN + "[OK] Option 82 is disabled, reducing potential issues." + Style.RESET_ALL) 1162 | 1163 | if snooping_database: 1164 | # Confirm that a DHCP Snooping binding database is set 1165 | print(Fore.GREEN + f"[OK] DHCP Snooping binding database is set to: {snooping_database.group(1)}" + Style.RESET_ALL) 1166 | else: 1167 | # Warn if no DHCP Snooping binding database is set (bindings will be lost on reboot) 1168 | issues.append("[!] No DHCP Snooping database configured! Snooping bindings will be lost on reboot.") 1169 | 1170 | if write_delay_match: 1171 | # Display the configured DHCP Snooping write-delay 1172 | write_delay_value = write_delay_match.group(1) 1173 | print(Fore.GREEN + f"[OK] DHCP Snooping write-delay is set to {write_delay_value} seconds." + Style.RESET_ALL) 1174 | else: 1175 | # Warn if no write-delay is configured 1176 | print(Fore.YELLOW + "[!] WARNING: No DHCP Snooping write-delay configured. Changes may not be written efficiently!" + Style.RESET_ALL) 1177 | 1178 | if rate_limit: 1179 | # Confirm that DHCP Snooping rate limiting is enabled 1180 | print(Fore.GREEN + f"[OK] DHCP Snooping rate limit is set on {len(rate_limit)} ports." + Style.RESET_ALL) 1181 | else: 1182 | # Recommend enabling DHCP Snooping rate limiting to prevent DHCP starvation attacks 1183 | notices.append("[*] No rate limiting on DHCP Snooping. Consider enabling to prevent DHCP starvation attacks.") 1184 | 1185 | if issues: 1186 | # Display warnings for detected security risks 1187 | print(Fore.YELLOW + "[!] WARNING: DHCP Snooping security issues detected:" + Style.RESET_ALL) 1188 | for issue in issues: 1189 | print(Fore.YELLOW + " - " + issue + Style.RESET_ALL) 1190 | 1191 | if notices: 1192 | # Display additional recommendations for best practices 1193 | print(Fore.YELLOW + "[*] Additional DHCP Snooping recommendations:" + Style.RESET_ALL) 1194 | for notice in notices: 1195 | print(Fore.WHITE + " - " + notice + Style.RESET_ALL) 1196 | 1197 | if not issues: 1198 | # Confirm that no critical DHCP Snooping security issues were detected 1199 | print(Fore.GREEN + "[OK] No critical security issues found with DHCP Snooping." + Style.RESET_ALL) 1200 | 1201 | # Checking DAI 1202 | def checking_dai(connection): 1203 | # Prints a visual separator for clarity 1204 | print_separator() 1205 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking Dynamic ARP Inspection" + Style.RESET_ALL) 1206 | 1207 | try: 1208 | # Retrieve Dynamic ARP Inspection (DAI) configuration from the running configuration 1209 | dai_config = connection.send_command("show running-config | section arp inspection").strip() 1210 | 1211 | # If no DAI configuration is found, assume the feature is not enabled 1212 | if not dai_config: 1213 | print(Fore.RED + "[!] WARNING: Dynamic ARP Inspection (DAI) is NOT enabled on this device!" + Style.RESET_ALL) 1214 | return 1215 | 1216 | except Exception as e: 1217 | # Handle errors if the command execution fails 1218 | print(Fore.RED + f"[-] Failed to retrieve DAI configuration: {e}" + Style.RESET_ALL) 1219 | return 1220 | 1221 | # Extract VLANs where DAI is enabled 1222 | dai_vlans = re.findall(r'ip arp inspection vlan (\S+)', dai_config) 1223 | 1224 | # Extract trusted interfaces for ARP inspection 1225 | trusted_ports = re.findall(r'ip arp inspection trust', dai_config) 1226 | 1227 | # Extract ARP filters applied for additional security 1228 | arp_filters = re.findall(r'ip arp inspection filter (\S+)', dai_config) 1229 | 1230 | # Extract ARP inspection rate limits 1231 | arp_rate_limits = re.findall(r'ip arp inspection limit rate (\d+)', dai_config) 1232 | 1233 | issues = [] # Stores critical DAI misconfigurations 1234 | notices = [] # Stores best practice recommendations 1235 | 1236 | if not dai_vlans: 1237 | # Warn if DAI is enabled but no VLANs are configured 1238 | issues.append("[!] DAI is enabled, but no VLANs are configured!") 1239 | else: 1240 | print(Fore.GREEN + f"[OK] DAI is enabled for VLANs: {', '.join(dai_vlans)}" + Style.RESET_ALL) 1241 | 1242 | if not trusted_ports: 1243 | # Warn if no trusted interfaces are configured 1244 | issues.append("[!] No trusted ports configured! DHCP server responses may be blocked.") 1245 | else: 1246 | print(Fore.YELLOW + f"[*] Trusted ports are configured on {len(trusted_ports)} interfaces." + Style.RESET_ALL) 1247 | 1248 | if arp_filters: 1249 | # Confirm that ARP inspection filters are applied 1250 | print(Fore.GREEN + f"[OK] ARP inspection filter(s) configured: {', '.join(arp_filters)}" + Style.RESET_ALL) 1251 | else: 1252 | # Recommend configuring ARP inspection filters for enhanced security 1253 | notices.append("[*] No ARP inspection filters configured. Consider adding ACLs for better security.") 1254 | 1255 | if arp_rate_limits: 1256 | # Confirm that ARP inspection rate limiting is enabled 1257 | print(Fore.GREEN + f"[OK] ARP inspection rate limit is set on {len(arp_rate_limits)} ports." + Style.RESET_ALL) 1258 | else: 1259 | # Recommend enabling ARP rate limiting to prevent ARP flooding attacks 1260 | notices.append("[*] No rate limiting on ARP inspection. Consider setting limits to prevent flooding.") 1261 | 1262 | if issues: 1263 | # Display warnings for detected security risks 1264 | print(Fore.YELLOW + "[!] WARNING: DAI security issues detected!" + Style.RESET_ALL) 1265 | for issue in issues: 1266 | print(Fore.YELLOW + " - " + issue + Style.RESET_ALL) 1267 | 1268 | if notices: 1269 | # Display additional recommendations for best practices 1270 | print(Fore.YELLOW + "[*] NOTICE: Additional DAI recommendations:" + Style.RESET_ALL) 1271 | for notice in notices: 1272 | print(Fore.YELLOW + " - " + notice + Style.RESET_ALL) 1273 | 1274 | if not issues: 1275 | # Confirm that no critical DAI security issues were detected 1276 | print(Fore.GREEN + "[OK] No critical security issues found with DAI configuration." + Style.RESET_ALL) 1277 | 1278 | # Checking BPDU Guard 1279 | def checking_bpdu_guard(connection): 1280 | # Prints a visual separator for clarity 1281 | print_separator() 1282 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking BPDU Guard Operation" + Style.RESET_ALL) 1283 | 1284 | try: 1285 | # Retrieve BPDU Guard global setting 1286 | bpdu_global = connection.send_command("show running-config | include spanning-tree portfast bpduguard default").strip() 1287 | 1288 | # Retrieve interface-level BPDU Guard settings 1289 | bpdu_interfaces = connection.send_command("show running-config | section interface").strip() 1290 | 1291 | # Identify interfaces where BPDU Guard is explicitly enabled 1292 | bpdu_enabled_ports = re.findall(r'interface (\S+)\n.*?spanning-tree bpduguard enable', bpdu_interfaces, re.DOTALL) 1293 | 1294 | except Exception as e: 1295 | # Handle errors if the command execution fails 1296 | print(Fore.RED + f"[-] Failed to retrieve BPDU Guard configuration: {e}" + Style.RESET_ALL) 1297 | return 1298 | 1299 | if bpdu_global: 1300 | # Confirm that BPDU Guard is enabled globally 1301 | print(Fore.GREEN + "[OK] BPDU Guard is enabled globally (portfast default)" + Style.RESET_ALL) 1302 | 1303 | if bpdu_enabled_ports: 1304 | # Confirm that BPDU Guard is enabled on specific interfaces 1305 | print(Fore.GREEN + "[OK] BPDU Guard is enabled on the following interfaces:" + Style.RESET_ALL) 1306 | for interface in bpdu_enabled_ports: 1307 | print(Fore.GREEN + f" - {interface}" + Style.RESET_ALL) 1308 | 1309 | if not bpdu_global and not bpdu_enabled_ports: 1310 | # Warn if BPDU Guard is not enabled globally or on any interfaces 1311 | print(Fore.RED + "[!] WARNING: BPDU Guard is NOT enabled on any interface!" + Style.RESET_ALL) 1312 | print(Fore.RED + " - Without BPDU Guard, an attacker can exploit STP, hijack the root switch role, and perform a partial MITM attack." + Style.RESET_ALL) 1313 | print(Fore.RED + " - Consider enabling BPDU Guard on all edge ports to prevent unauthorized STP participation." + Style.RESET_ALL) 1314 | 1315 | # Checking SMI 1316 | def checking_smart_install(connection): 1317 | # Prints a visual separator for clarity 1318 | print_separator() 1319 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking Smart Install Operation" + Style.RESET_ALL) 1320 | 1321 | try: 1322 | # Retrieve Smart Install configuration using 'show vstack config' 1323 | vstack_config = connection.send_command("show vstack config").strip() 1324 | except Exception as e: 1325 | # If the command is not recognized, Smart Install is not supported on this device 1326 | if "Invalid input detected" in str(e): 1327 | print(Fore.WHITE + "[*] This device does not support Smart Install (no `show vstack config` command)." + Style.RESET_ALL) 1328 | return 1329 | else: 1330 | # Handle errors if the command execution fails 1331 | print(Fore.RED + f"[-] Failed to retrieve Smart Install configuration: {e}" + Style.RESET_ALL) 1332 | return 1333 | 1334 | # Check if Smart Install is enabled or disabled 1335 | enabled_match = re.search(r'Oper Mode:\s+Enabled', vstack_config) 1336 | disabled_match = re.search(r'Oper Mode:\s+Disabled', vstack_config) 1337 | 1338 | # Retrieve running-config to check if Smart Install is explicitly disabled 1339 | running_config = connection.send_command("show running-config | include vstack").strip() 1340 | explicit_disable = "no vstack" in running_config 1341 | 1342 | if enabled_match: 1343 | # Warn if Smart Install is enabled, as it is a known security risk 1344 | print(Fore.RED + "[!] WARNING: Smart Install is ENABLED! Device is vulnerable to remote exploitation." + Style.RESET_ALL) 1345 | print(Fore.RED + " - Oper Mode: Enabled" + Style.RESET_ALL) 1346 | print(Fore.RED + " - RECOMMENDATION: Disable Smart Install immediately using `no vstack`." + Style.RESET_ALL) 1347 | elif disabled_match or explicit_disable: 1348 | # Confirm that Smart Install is disabled 1349 | print(Fore.GREEN + "[OK] Smart Install is disabled. No security risks detected." + Style.RESET_ALL) 1350 | else: 1351 | # Notify if the status of Smart Install cannot be determined 1352 | print(Fore.YELLOW + "[*] NOTICE: Unable to determine Smart Install status. Manual check recommended." + Style.RESET_ALL) 1353 | 1354 | # Checking Storm-Control 1355 | def checking_storm_control(connection): 1356 | # Prints a visual separator for clarity 1357 | print_separator() 1358 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking Storm-Control Operation" + Style.RESET_ALL) 1359 | 1360 | try: 1361 | # Ensure the device is in privileged exec mode 1362 | connection.enable() 1363 | connection.send_command("terminal length 0") 1364 | 1365 | # Retrieve Storm-Control settings from the device 1366 | storm_control_output = connection.send_command("show storm-control").strip() 1367 | 1368 | # If the command is invalid or there is no output, assume Storm-Control is not enabled or supported 1369 | if "Invalid input" in storm_control_output or not storm_control_output: 1370 | print(Fore.RED + "[!] WARNING: Storm-Control is NOT supported or not enabled on this device!" + Style.RESET_ALL) 1371 | return 1372 | except Exception as e: 1373 | # Handle errors if the command execution fails 1374 | print(Fore.RED + f"[-] Failed to retrieve Storm-Control configuration: {e}" + Style.RESET_ALL) 1375 | return 1376 | 1377 | # Regex pattern to extract interface settings from the storm-control output 1378 | pattern = re.compile( 1379 | r'^(\S+)\s+Link\s+\S+\s+(\d+\.\d+%)\s+(\d+\.\d+%)\s+(\d+\.\d+%)\s+(\S+)\s+([BUM])' 1380 | ) 1381 | 1382 | storm_interfaces = {} 1383 | 1384 | for line in storm_control_output.splitlines(): 1385 | match = pattern.search(line) 1386 | if match: 1387 | interface, upper, lower, current, action, traffic_type = match.groups() 1388 | 1389 | # Initialize the interface entry if not present 1390 | if interface not in storm_interfaces: 1391 | storm_interfaces[interface] = { 1392 | "Broadcast": None, 1393 | "Multicast": None, 1394 | "Unicast": None, 1395 | "Action": action 1396 | } 1397 | 1398 | # Assign limits based on traffic type (B - Broadcast, M - Multicast, U - Unicast) 1399 | if traffic_type == "B": 1400 | storm_interfaces[interface]["Broadcast"] = f"{upper} (Lower: {lower}, Current: {current})" 1401 | elif traffic_type == "M": 1402 | storm_interfaces[interface]["Multicast"] = f"{upper} (Lower: {lower}, Current: {current})" 1403 | elif traffic_type == "U": 1404 | storm_interfaces[interface]["Unicast"] = f"{upper} (Lower: {lower}, Current: {current})" 1405 | 1406 | no_action_interfaces = [] 1407 | 1408 | # Identify interfaces where no action is set for Storm-Control violations 1409 | for interface, settings in storm_interfaces.items(): 1410 | if settings["Action"].lower() == "none": 1411 | no_action_interfaces.append(interface) 1412 | 1413 | if storm_interfaces: 1414 | # Confirm that Storm-Control is enabled on interfaces and display their settings 1415 | print(Fore.GREEN + "[OK] Storm-Control is enabled on the following interfaces:" + Style.RESET_ALL) 1416 | for interface, settings in storm_interfaces.items(): 1417 | print(Fore.GREEN + f" - {interface}:" + Style.RESET_ALL) 1418 | for t_type, limit_str in settings.items(): 1419 | if t_type != "Action" and limit_str: 1420 | print(Fore.GREEN + f" {t_type}: {limit_str}" + Style.RESET_ALL) 1421 | print(Fore.GREEN + f" Action: {settings['Action']}" + Style.RESET_ALL) 1422 | else: 1423 | # Warn if Storm-Control is not enabled on any interfaces 1424 | print(Fore.RED + "[!] WARNING: Storm-Control is NOT enabled on this device!" + Style.RESET_ALL) 1425 | 1426 | if no_action_interfaces: 1427 | # Warn if Storm-Control is enabled but no action is configured for violations 1428 | print(Fore.YELLOW + "[!] WARNING: Some interfaces have no storm-control action set!" + Style.RESET_ALL) 1429 | for intf in no_action_interfaces: 1430 | print(Fore.YELLOW + f" - {intf}: No storm-control action set!" + Style.RESET_ALL) 1431 | 1432 | # Checking Port-Security 1433 | def checking_port_security(connection): 1434 | # Prints a visual separator for clarity 1435 | print_separator() 1436 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking Port-Security Operation" + Style.RESET_ALL) 1437 | 1438 | try: 1439 | # Retrieve Port Security status from the device 1440 | port_security_output = connection.send_command("show port-security").strip() 1441 | 1442 | # If the command is invalid or there is no output, assume Port Security is not supported or enabled 1443 | if "Invalid input" in port_security_output or not port_security_output: 1444 | print(Fore.RED + "[!] WARNING: Port Security is NOT supported on this device!" + Style.RESET_ALL) 1445 | return 1446 | 1447 | except Exception as e: 1448 | # Handle errors if the command execution fails 1449 | print(Fore.RED + f"[-] Failed to retrieve Port Security configuration: {e}" + Style.RESET_ALL) 1450 | return 1451 | 1452 | port_security_status = {} 1453 | 1454 | # Parse each line of the output to extract relevant details 1455 | for line in port_security_output.splitlines(): 1456 | match = re.search(r'(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)', line) 1457 | if match: 1458 | interface, max_addr, current_addr, violations, action = match.groups() 1459 | port_security_status[interface] = { 1460 | "Max MAC": max_addr, 1461 | "Current MAC": current_addr, 1462 | "Violations": violations, 1463 | "Action": action 1464 | } 1465 | 1466 | if port_security_status: 1467 | # Confirm that Port Security is enabled and display its configuration per interface 1468 | print(Fore.GREEN + "[OK] Port Security is enabled on the following interfaces:" + Style.RESET_ALL) 1469 | for interface, details in port_security_status.items(): 1470 | print(Fore.GREEN + f" - {interface}: " + Style.RESET_ALL) 1471 | print(Fore.GREEN + f" Max Secure MACs: {details['Max MAC']}" + Style.RESET_ALL) 1472 | print(Fore.GREEN + f" Current MACs: {details['Current MAC']}" + Style.RESET_ALL) 1473 | 1474 | # Highlight any security violations on interfaces 1475 | if int(details["Violations"]) > 0: 1476 | print(Fore.YELLOW + f" Violations: {details['Violations']}" + Style.RESET_ALL) 1477 | 1478 | print(Fore.GREEN + f" Security Action: {details['Action']}" + Style.RESET_ALL) 1479 | else: 1480 | # Warn if no interfaces have Port Security enabled 1481 | print(Fore.RED + "[!] WARNING: No interfaces with Port Security enabled!" + Style.RESET_ALL) 1482 | 1483 | # Checking OSPF Passive Interfaces 1484 | def checking_ospf_passive(connection): 1485 | # Prints a visual separator for clarity 1486 | print_separator() 1487 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking OSPF Passive Interfaces" + Style.RESET_ALL) 1488 | 1489 | try: 1490 | # Retrieve OSPF process information 1491 | ospf_processes_output = connection.send_command("show running-config | include router ospf").strip() 1492 | ospf_processes = re.findall(r"router ospf (\d+)", ospf_processes_output) 1493 | 1494 | # If no OSPF processes are detected, assume OSPF is not in use 1495 | if not ospf_processes: 1496 | print(Fore.GREEN + "[!] No OSPF processes detected on this device." + Style.RESET_ALL) 1497 | return 1498 | 1499 | # Retrieve OSPF interface and configuration details 1500 | ospf_interfaces_output = connection.send_command("show ip ospf interface brief").strip() 1501 | ospf_config_output = connection.send_command("show running-config | section router ospf").strip() 1502 | 1503 | except Exception as e: 1504 | # Handle errors if the command execution fails 1505 | print(Fore.RED + f"[-] Failed to retrieve OSPF configuration: {e}" + Style.RESET_ALL) 1506 | return 1507 | 1508 | warnings = [] 1509 | 1510 | # Identify passive interfaces in OSPF configuration 1511 | passive_interfaces = re.findall(r"passive-interface (\S+)", ospf_config_output) 1512 | passive_interfaces = [iface.replace("Vlan", "Vl") for iface in passive_interfaces] # Adjust VLAN notation 1513 | 1514 | # Extract OSPF-enabled interfaces and their associated IPs 1515 | ospf_interfaces = re.findall(r"(\S+)\s+\d+\s+\d+\s+([\d\.\/]+)", ospf_interfaces_output) 1516 | 1517 | # Identify interfaces that should be passive but are not 1518 | for interface, ip in ospf_interfaces: 1519 | if interface not in passive_interfaces: 1520 | warnings.append(f"[WARNING] OSPF: Interface {interface} ({ip}) is not passive!") 1521 | 1522 | if warnings: 1523 | # Warn if any OSPF interfaces are not set to passive 1524 | print(Fore.YELLOW + "[!] WARNING: Some OSPF interfaces are not passive!" + Style.RESET_ALL) 1525 | for warning in warnings: 1526 | print(Fore.YELLOW + warning + Style.RESET_ALL) 1527 | 1528 | print(Fore.YELLOW + "[!] Enable passive interfaces for those networks where you don't want to see a malicious router." + Style.RESET_ALL) 1529 | print(Fore.GREEN + "[*] Passive interfaces help defend against attacks on dynamic routing domains." + Style.RESET_ALL) 1530 | 1531 | # Checking OSPF Authentication 1532 | def checking_ospf_auth(connection): 1533 | # Prints a visual separator for clarity 1534 | print_separator() 1535 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking OSPF Authentication" + Style.RESET_ALL) 1536 | 1537 | try: 1538 | # Retrieve OSPF interfaces brief output 1539 | ospf_if_brief_output = connection.send_command("show ip ospf interface brief") 1540 | except Exception as e: 1541 | # Handle errors if command execution fails 1542 | print(Fore.RED + f"[-] Failed to retrieve OSPF interface list: {e}" + Style.RESET_ALL) 1543 | return 1544 | 1545 | # Regular expression to extract interface details from OSPF interface brief output 1546 | pattern = r'^(\S+)\s+(\d+)\s+(\S+)\s+(\S+/\S+)\s+\S+\s+\S+\s+\S+/\S+' 1547 | matches = re.findall(pattern, ospf_if_brief_output, re.MULTILINE) 1548 | 1549 | if not matches: 1550 | # If no OSPF interfaces are found, exit function 1551 | print(Fore.GREEN + "[*] No OSPF interfaces found." + Style.RESET_ALL) 1552 | return 1553 | 1554 | # Lists to categorize authentication types per interface 1555 | md5_list = [] 1556 | simple_list = [] 1557 | none_list = [] 1558 | 1559 | # Iterate through each OSPF interface and check authentication status 1560 | for intf_name, pid, area, ip_mask in matches: 1561 | try: 1562 | # Retrieve detailed OSPF interface information 1563 | ospf_if_output = connection.send_command(f"show ip ospf interface {intf_name}") 1564 | except Exception as e: 1565 | # Handle errors if interface-specific command execution fails 1566 | print(Fore.RED + f"[-] Failed to check interface {intf_name}: {e}" + Style.RESET_ALL) 1567 | continue 1568 | 1569 | # Categorize authentication methods based on command output 1570 | if "Cryptographic authentication enabled" in ospf_if_output: 1571 | md5_list.append((intf_name, ip_mask)) 1572 | elif "Simple password authentication enabled" in ospf_if_output: 1573 | simple_list.append((intf_name, ip_mask)) 1574 | else: 1575 | none_list.append((intf_name, ip_mask)) 1576 | 1577 | # Display warnings if insecure or missing authentication is detected 1578 | if none_list or simple_list: 1579 | print(Fore.RED + "[!] WARNING: Some OSPF interfaces have no or insecure authentication!" + Style.RESET_ALL) 1580 | 1581 | for intf_name, ip_mask in none_list: 1582 | print(Fore.RED + f"[WARNING] OSPF: Interface {intf_name} ({ip_mask}) has no authentication!" + Style.RESET_ALL) 1583 | 1584 | for intf_name, ip_mask in simple_list: 1585 | print(Fore.RED + f"[WARNING] OSPF: Interface {intf_name} ({ip_mask}) has Simple authentication!" + Style.RESET_ALL) 1586 | 1587 | # Display secure authentication configuration 1588 | for intf_name, ip_mask in md5_list: 1589 | print(Fore.GREEN + f"[OK] OSPF: Interface {intf_name} ({ip_mask}) has MD5 authentication!" + Style.RESET_ALL) 1590 | 1591 | # Checking EIGRP Passive Interfaces 1592 | def checking_eigrp_passive(connection): 1593 | # Prints a visual separator for clarity 1594 | print_separator() 1595 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking EIGRP Passive Interfaces" + Style.RESET_ALL) 1596 | 1597 | try: 1598 | # Retrieve EIGRP process information 1599 | eigrp_processes_output = connection.send_command("show running-config | include router eigrp").strip() 1600 | eigrp_processes = re.findall(r"router eigrp (\d+)", eigrp_processes_output) 1601 | 1602 | # If no EIGRP processes are detected, assume EIGRP is not in use 1603 | if not eigrp_processes: 1604 | print(Fore.GREEN + "No EIGRP processes detected on this device." + Style.RESET_ALL) 1605 | return 1606 | 1607 | # Retrieve EIGRP interfaces and configuration details 1608 | eigrp_interfaces_output = connection.send_command("show ip eigrp interfaces").strip() 1609 | eigrp_config_output = connection.send_command("show running-config | section router eigrp").strip() 1610 | 1611 | except Exception as e: 1612 | # Handle errors if the command execution fails 1613 | print(Fore.RED + f"[-] Failed to retrieve EIGRP configuration: {e}" + Style.RESET_ALL) 1614 | return 1615 | 1616 | # Identify passive interfaces in EIGRP configuration 1617 | passive_interfaces = re.findall(r"passive-interface (\S+)", eigrp_config_output) 1618 | 1619 | # Extract EIGRP-enabled interfaces 1620 | eigrp_interfaces = re.findall(r"^(\S+)\s+\d+", eigrp_interfaces_output, re.MULTILINE) 1621 | 1622 | warnings = [] 1623 | 1624 | # Identify interfaces that should be passive but are not 1625 | for interface in eigrp_interfaces: 1626 | if interface not in passive_interfaces: 1627 | warnings.append(f"[WARNING] EIGRP: Interface {interface} is not passive!") 1628 | 1629 | if warnings: 1630 | # Warn if any EIGRP interfaces are not set to passive 1631 | print(Fore.YELLOW + "[!] WARNING: Some EIGRP interfaces are not passive!" + Style.RESET_ALL) 1632 | for warning in warnings: 1633 | print(Fore.YELLOW + warning + Style.RESET_ALL) 1634 | 1635 | print(Fore.YELLOW + "[!] Enable passive interfaces for those networks where you don't want to see a malicious router." + Style.RESET_ALL) 1636 | print(Fore.GREEN + "[*] Passive interfaces help defend against attacks on dynamic routing domains." + Style.RESET_ALL) 1637 | 1638 | # Checking EIGRP Authentication 1639 | def checking_eigrp_auth(connection): 1640 | # Prints a visual separator for clarity 1641 | print_separator() 1642 | print(Fore.WHITE + Style.BRIGHT + "[*] Checking EIGRP Authentication" + Style.RESET_ALL) 1643 | 1644 | try: 1645 | # Retrieve a list of interfaces running EIGRP 1646 | eigrp_interfaces_output = connection.send_command("show ip eigrp interfaces").strip() 1647 | except Exception as e: 1648 | # Handle errors if the command execution fails 1649 | print(Fore.RED + f"[-] Failed to retrieve EIGRP interfaces: {e}" + Style.RESET_ALL) 1650 | return 1651 | 1652 | # Extract EIGRP-enabled interfaces 1653 | eigrp_interfaces = re.findall(r"^(\S+)\s+\d+", eigrp_interfaces_output, re.MULTILINE) 1654 | 1655 | if not eigrp_interfaces: 1656 | # If no EIGRP interfaces are found, exit 1657 | print(Fore.GREEN + "[*] No EIGRP interfaces found." + Style.RESET_ALL) 1658 | return 1659 | 1660 | md5_kc_list = [] # Interfaces with MD5 authentication and key-chain 1661 | md5_only_list = [] # Interfaces with MD5 authentication but no key-chain 1662 | none_list = [] # Interfaces with no authentication 1663 | 1664 | for interface in eigrp_interfaces: 1665 | try: 1666 | # Retrieve the running configuration for each interface 1667 | run_int_output = connection.send_command(f"show run interface {interface}") 1668 | except Exception as e: 1669 | # Handle errors if unable to check the interface 1670 | print(Fore.RED + f"[-] Failed to check interface {interface}: {e}" + Style.RESET_ALL) 1671 | continue 1672 | 1673 | # Search for MD5 authentication mode 1674 | md5_mode = re.search(r"ip authentication mode eigrp (\d+) md5", run_int_output) 1675 | # Search for a key-chain used with MD5 authentication 1676 | key_chain = re.search(r"ip authentication key-chain eigrp (\d+) (\S+)", run_int_output) 1677 | 1678 | if md5_mode and key_chain: 1679 | # If both MD5 mode and key-chain are found 1680 | md5_kc_list.append((interface, key_chain.group(2))) 1681 | elif md5_mode: 1682 | # If MD5 authentication is found but no key-chain 1683 | md5_only_list.append(interface) 1684 | else: 1685 | # If no authentication is configured 1686 | none_list.append(interface) 1687 | 1688 | # Display warnings if any interfaces lack authentication 1689 | if none_list: 1690 | print(Fore.RED + "[!] WARNING: Some EIGRP interfaces have no authentication!" + Style.RESET_ALL) 1691 | for interface in none_list: 1692 | print(Fore.RED + f"[WARNING] EIGRP: Interface {interface} has no authentication!" + Style.RESET_ALL) 1693 | 1694 | # Display interfaces with MD5 authentication but no key-chain 1695 | for interface in md5_only_list: 1696 | print(Fore.GREEN + f"[OK] EIGRP: Interface {interface} has MD5 mode configured (no key-chain detected)!" + Style.RESET_ALL) 1697 | 1698 | # Display interfaces with MD5 authentication and an assigned key-chain 1699 | for interface, chain_name in md5_kc_list: 1700 | print(Fore.GREEN + f"[OK] EIGRP: Interface {interface} has MD5 mode with key-chain '{chain_name}'!" + Style.RESET_ALL) 1701 | 1702 | # Outro 1703 | def analysis_summary(start_time, device_type): 1704 | """ 1705 | Prints a summary of the Cisco IOS security inspection, including elapsed time and device type. 1706 | """ 1707 | # Calculate the total execution time 1708 | end_time = datetime.datetime.now() 1709 | elapsed_time = round((end_time - start_time).total_seconds(), 2) 1710 | 1711 | # Print completion banner 1712 | print("=" * 60) 1713 | print(Fore.CYAN + Style.BRIGHT + "[*] Cisco IOS Security Inspection COMPLETED" + Style.RESET_ALL) 1714 | 1715 | # Display the time taken for the inspection 1716 | print(Fore.GREEN + f"[*] Time taken: {elapsed_time} seconds" + Style.RESET_ALL) 1717 | 1718 | # Determine the device type (Router or Switch) 1719 | device_type_str = "Router" if device_type == "router" else "Switch" 1720 | print(Fore.WHITE + f"[*] Device Type: {device_type_str}" + Style.RESET_ALL) 1721 | 1722 | # Display a cautionary note for reviewing findings 1723 | print(Fore.YELLOW + "[!] Review the results carefully and apply necessary fixes" + Style.RESET_ALL) 1724 | 1725 | # Print closing separator 1726 | print("=" * 60) 1727 | 1728 | # Visual separator 1729 | def print_stage_separator(title="CHECKS", color=Fore.CYAN, width=30): 1730 | """ 1731 | Prints a formatted section separator with a title for better readability of output. 1732 | 1733 | Args: 1734 | title (str): The title displayed in the separator. 1735 | color (Fore): The color of the separator text. 1736 | width (int): The width of the separator lines. 1737 | """ 1738 | print() # Add an empty line for spacing 1739 | 1740 | # Create the top and bottom separator line 1741 | top_bottom_line = "[" + "=" * width + "]" 1742 | 1743 | # Format the title line with a centered text and vertical bars 1744 | middle_line = "|" + title.center(width) + "|" 1745 | 1746 | # Print the formatted separator with the specified color 1747 | print(color + top_bottom_line + Style.RESET_ALL) 1748 | print(color + middle_line + Style.RESET_ALL) 1749 | print(color + top_bottom_line + Style.RESET_ALL) 1750 | 1751 | print() # Add an empty line for spacing 1752 | 1753 | # Main func 1754 | def main(): 1755 | """ 1756 | Main function to execute Cisco IOS Security Inspection. 1757 | It connects to the target device, performs multiple security checks, and prints a summary. 1758 | """ 1759 | # Display banner 1760 | banner() 1761 | 1762 | # Argument parser to handle command-line options 1763 | parser = argparse.ArgumentParser() 1764 | parser.add_argument("--ip", required=True, help="Specify the IP address of the device") 1765 | parser.add_argument("--username", required=True, help="SSH Username") 1766 | parser.add_argument("--password", required=True, help="SSH Password") 1767 | parser.add_argument("--port", type=int, default=22, help="SSH Port (default:22)") 1768 | parser.add_argument("--router", action="store_true", help="Specify if the device is a router") 1769 | parser.add_argument("--l2-switch", action="store_true", help="Specify if the device is a L2 switch") 1770 | parser.add_argument("--l3-switch", action="store_true", help="Specify if the device is a L3 switch") 1771 | args = parser.parse_args() 1772 | 1773 | # Ensure that exactly one device type is selected 1774 | flags = [args.router, args.l2_switch, args.l3_switch] 1775 | if sum(flags) != 1: 1776 | print(Fore.YELLOW + "[!] You must specify exactly one device type: --router, --l2-switch, or --l3-switch." + Style.RESET_ALL) 1777 | exit(1) 1778 | 1779 | # Determine the device type based on the argument 1780 | if args.router: 1781 | device_type = "router" 1782 | elif args.l2_switch: 1783 | device_type = "l2-switch" 1784 | else: 1785 | device_type = "l3-switch" 1786 | 1787 | # Track execution time 1788 | start_time = datetime.datetime.now() 1789 | 1790 | # Establish SSH connection to the target device 1791 | conn = connect_to_device(args.ip, args.username, args.password, args.port, device_type) 1792 | 1793 | # Perform system and operational checks 1794 | print_stage_separator("SYSTEM & OPERATIONAL CHECKS", color=Fore.MAGENTA, width=50) 1795 | check_device_uptime(conn) 1796 | checking_config_size(conn) 1797 | 1798 | # Perform Cisco IOS security analysis 1799 | print_stage_separator("IOS SECURITY ANALYZING", color=Fore.MAGENTA, width=50) 1800 | checking_pad_service(conn) 1801 | checking_service_password_encryption(conn) 1802 | checking_password_hashing(conn) 1803 | checking_rbac(conn) 1804 | checking_vty_security(conn) 1805 | checking_aaa(conn) 1806 | checking_session_limit(conn) 1807 | checking_login_block_protection(conn) 1808 | checking_ssh_security(conn) 1809 | checking_default_usernames(conn) 1810 | checking_snmp(conn) 1811 | checking_smart_install(conn) 1812 | 1813 | # Perform Layer 2 security checks if the device is a switch 1814 | if args.l2_switch or args.l3_switch: 1815 | print_stage_separator("L2 SECURITY ANALYZING", color=Fore.MAGENTA, width=50) 1816 | checking_vtp_status(conn) 1817 | checking_dtp_status(conn) 1818 | checking_native_vlan(conn) 1819 | checking_dhcp_snooping(conn) 1820 | checking_dai(conn) 1821 | checking_bpdu_guard(conn) 1822 | checking_storm_control(conn) 1823 | checking_port_security(conn) 1824 | 1825 | # Perform Layer 3 security checks if the device is a router or L3 switch 1826 | if args.router or args.l3_switch: 1827 | print_stage_separator("L3 SECURITY ANALYZING", color=Fore.MAGENTA, width=50) 1828 | checking_hsrp(conn) 1829 | checking_vrrp(conn) 1830 | checking_glbp(conn) 1831 | checking_ospf_passive(conn) 1832 | checking_ospf_auth(conn) 1833 | checking_eigrp_passive(conn) 1834 | checking_eigrp_auth(conn) 1835 | 1836 | # Disconnect from the device 1837 | if conn: 1838 | conn.disconnect() 1839 | 1840 | # Display the final analysis summary 1841 | analysis_summary(start_time, device_type) 1842 | 1843 | if __name__ == "__main__": 1844 | main() 1845 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="nihilist", 5 | version="1.0", 6 | url="https://github.com/casterbyte/Nihilist", 7 | author="Magama Bazarov", 8 | author_email="magamabazarov@mailbox.org", 9 | description="Cisco IOS Security Inspector", 10 | long_description=open('README.md', encoding="utf8").read(), 11 | long_description_content_type='text/markdown', 12 | license="Apache-2.0", 13 | keywords=['network security', 'config analyzer', 'cisco', 'cisco ios', 'hardening', 'networks'], 14 | install_requires=[ 15 | 'colorama', 16 | 'netmiko', 17 | ], 18 | py_modules=['nihilist'], 19 | entry_points={ 20 | "console_scripts": ["nihilist = nihilist:main"], 21 | }, 22 | python_requires='>=3.11', 23 | ) -------------------------------------------------------------------------------- /visuals/nihilist_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casterbyte/Nihilist/0205dde457d7df6bf59cf4ddf733e21280b9a205/visuals/nihilist_card.png -------------------------------------------------------------------------------- /visuals/nihilist_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casterbyte/Nihilist/0205dde457d7df6bf59cf4ddf733e21280b9a205/visuals/nihilist_demo.gif --------------------------------------------------------------------------------