mirror of
https://github.com/casterbyte/Sara.git
synced 2025-12-17 11:59:30 +00:00
v1.3
This commit is contained in:
parent
93f970d6eb
commit
fc94270c0e
6 changed files with 1166 additions and 890 deletions
264
README.md
264
README.md
|
|
@ -1,15 +1,21 @@
|
||||||
# Sara: RouterOS Security Inspector
|
# Sara: MikroTik RouterOS Security Inspector
|
||||||
|
|
||||||
RouterOS configuration analyzer to find security misconfigurations and vulnerabilities.
|
RouterOS security analyzer for detecting misconfigurations, weak settings, and known vulnerabilities (CVE).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
```
|
```bash
|
||||||
RouterOS Security Inspector. Designed for security engineers
|
_____
|
||||||
|
/ ___/____ __________ _
|
||||||
|
\__ \/ __ `/ ___/ __ `/
|
||||||
|
___/ / /_/ / / / /_/ /
|
||||||
|
/____/\__,_/_/ \__,_/
|
||||||
|
|
||||||
Author: Mahama Bazarov, <mahamabazarov@mailbox.org>
|
Sara: MikroTik RouterOS Security Inspector
|
||||||
Alias: Caster
|
Developer: Mahama Bazarov (Caster)
|
||||||
Version: 1.2
|
Contact: mahamabazarov@mailbox.org
|
||||||
|
Version: 1.3.0
|
||||||
|
Documentation & Usage: https://github.com/caster0x00/Sara
|
||||||
```
|
```
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
|
|
@ -26,7 +32,7 @@ The author does not take any responsibility for the misuse of this tool, includi
|
||||||
|
|
||||||
# Sara is not an attack tool
|
# Sara is not an attack tool
|
||||||
|
|
||||||
**Sara does not bypass authentication, exploit vulnerabilities, or alter RouterOS configurations.** It works in **read-only mode**, requiring no administrative privileges.
|
**Sara does not bypass authentication, exploit vulnerabilities, or alter RouterOS configurations.** It works in read-only mode and does not modify device configuration. A read-only RouterOS account is sufficient.
|
||||||
|
|
||||||
If you are unsure about the interpretation of the analysis results, consult an experienced network engineer before making any decisions!
|
If you are unsure about the interpretation of the analysis results, consult an experienced network engineer before making any decisions!
|
||||||
|
|
||||||
|
|
@ -38,141 +44,95 @@ Before use, ensure that your device auditing complies with your organization's l
|
||||||
- Use it only on your devices or with the owner's permission;
|
- Use it only on your devices or with the owner's permission;
|
||||||
- Do not use Sara on other people's networks without the owner's explicit consent - this may violate computer security laws!
|
- Do not use Sara on other people's networks without the owner's explicit consent - this may violate computer security laws!
|
||||||
|
|
||||||
# Mechanism
|
# Features
|
||||||
|
|
||||||
**Sara** uses [netmiko](https://github.com/ktbyers/netmiko) to remotely connect via SSH to RouterOS devices. It executes RouterOS system commands to extract configuration data and analyze it for potential vulnerabilities and signs of compromise. The user connects to the hardware himself using Sara by entering his username and password. Sara executes exactly `print` based commands, thus not changing the configuration of your hardware in any way. So, by the way, you can even use an RO-only account if you want to.
|
**Sara** uses [netmiko](https://github.com/ktbyers/netmiko) to remotely connect via SSH to RouterOS devices. It executes RouterOS system commands to extract configuration data and analyze it for potential vulnerabilities and signs of compromise. The user connects to the hardware himself using Sara by entering his username and password. Sara executes only `print` commands and does not change RouterOS configuration in any way. You can even use a read-only (RO) account if you want to.
|
||||||
Sara does not use any exploits, payloads or bruteforce attacks. All RouterOS security analysis here is based on pure configuration analysis.
|
Sara does not use any exploits, payloads or bruteforce attacks. All RouterOS security analysis here is based on pure configuration analysis.
|
||||||
|
|
||||||
## What exactly is Sara checking for?
|
## Profiles
|
||||||
|
|
||||||
1. **SMB protocol activity** – determines whether SMB is enabled, which may be vulnerable to CVE-2018-7445;
|
Sara uses audit profiles. Each profile covers its own audit scope.
|
||||||
|
|
||||||
2. **Check the status of RMI interfaces** – identifies active management services (Telnet, FTP, Winbox, API, HTTP/HTTPS);
|
- `system`: This profile covers RouterOS system checks. It checks the system version, account status, remote management services and their availability, IP restrictions, PoE and RouterBOOT status, SSH settings, and password policies. It also analyzes MAC Winbox/Telnet services, NAT rules, connection tracking mode, RoMON, and the scheduler, including for suspicious automatic tasks and possible persistence mechanisms.
|
||||||
|
- `protocols`: The profile focuses on network protocols and services that are commonly used as entry points for attacks. It checks SMB, UPnP, SOCKS proxies, DNS settings and static DNS records, DDNS cloud services, Neighbor Discovery settings, and SNMP. The profile's task is to identify enabled or improperly restricted network services that could increase the attack surface.
|
||||||
|
- `wifi`: This profile evaluates the security of the wireless part of RouterOS. It analyzes Wi-Fi interfaces, PMKID parameters, and WPS modes that could open up opportunities for offline attacks or unauthorized connections. The profile helps you quickly understand whether the current Wi-Fi network security configuration is weak.
|
||||||
|
|
||||||
3. **Wi-Fi Security Check** – determines whether WPS and PMKID support are enabled, which can be used in WPA2-PSK attacks;
|
# CVE Search
|
||||||
|
|
||||||
> At the moment, this check has minor stability issues, as different versions of RouterOS have different variations of Wi-Fi configurations. Keep that in mind, but feel free to make an issue, we'll look into it;
|
|
||||||
|
|
||||||
4. **Check UPnP** – determines whether UPnP is enabled, which can automatically forward ports and threaten network security;
|
|
||||||
|
|
||||||
5. **Check DNS settings** – detects whether `allow-remote-requests`, which makes the router a DNS server, is enabled;
|
|
||||||
|
|
||||||
6. **Check DDNS** – determines whether dynamic DNS is enabled, which can reveal the real IP address of the device;
|
|
||||||
|
|
||||||
7. **PoE Test** – checks if PoE is enabled, which may cause damage to connected devices;
|
|
||||||
|
|
||||||
8. **Check RouterBOOT security** – determines if RouterBOOT bootloader protection is enabled;
|
|
||||||
|
|
||||||
9. **Check SOCKS Proxy** – identifies an active SOCKS Proxy that could be used by an attacker for pivoting, as well as indicating a potential compromise of the device.
|
|
||||||
|
|
||||||
10. **Bandwidth Server Test (BTest)** – determines whether a bandwidth server is enabled that can be used for a Flood attack by the attacker;
|
|
||||||
|
|
||||||
11. **Check discovery protocols** – determines whether CDP, LLDP, MNDP that can disclose network information are active;
|
|
||||||
|
|
||||||
12. **Check minimum password length** – determines whether the `minimum-password-length` parameter is set to prevent the use of weak passwords;
|
|
||||||
|
|
||||||
13. **SSH Check** – analyzes SSH settings, including the use of strong-crypto and Port Forwarding permission;
|
|
||||||
|
|
||||||
14. **Check Connection Tracking** – determines whether Connection Tracking is enabled, which can increase the load and open additional attack vectors;
|
|
||||||
|
|
||||||
15. **RoMON check** – detects RoMON activity, which allows you to manage devices at Layer 2;
|
|
||||||
|
|
||||||
16. **Check Winbox MAC Server** – analyzes access by MAC address via Winbox and Telnet, which can be a vulnerability on a local network;
|
|
||||||
|
|
||||||
17. **Check SNMP** – detects the use of weak SNMP community strings (`public`, `private`);
|
|
||||||
|
|
||||||
18. **Check NAT rules** – analyzes port forwarding (`dst-nat`, `netmap`) that may allow access to internal services from the outside;
|
|
||||||
|
|
||||||
19. **Check network access to RMI** – determines whether access to critical services (API, Winbox, SSH) is restricted to trusted IPs only;
|
|
||||||
|
|
||||||
20. **Check RouterOS version** – analyzes the current version of RouterOS and compares it to known vulnerable versions;
|
|
||||||
|
|
||||||
21. **RouterOS Vulnerability Check** – checks the RouterOS version against the CVE database and displays a list of known vulnerabilities;
|
|
||||||
|
|
||||||
22. **“Keep Password” in Winbox** – warns of potential use of the “Keep Password” feature
|
|
||||||
|
|
||||||
23. **Check default usernames** – defines the use of standard logins (`admin`, `engineer`, `test`, `mikrotik`);
|
|
||||||
|
|
||||||
24. **Checking the schedulers** – detects malicious tasks that can load remote scripts, perform hidden reboots, or run too often;
|
|
||||||
|
|
||||||
25. **Check static DNS records** – Analyzes static DNS records that can be used for phishing and MITM attacks.
|
|
||||||
|
|
||||||
## A breakdown of one technique
|
|
||||||
|
|
||||||
Sara analyzes MikroTik RouterOS configuration by sending commands via SSH and interpreting the results. Let's consider a basic example of checking an SMB service that may be vulnerable to CVE-2018-7445.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# SMB Check
|
|
||||||
def check_smb(connection):
|
|
||||||
separator("Checking SMB Service")
|
|
||||||
command = "/ip smb print"
|
|
||||||
output = connection.send_command(command)
|
|
||||||
|
|
||||||
if "enabled: yes" in output:
|
|
||||||
print(Fore.RED + Style.BRIGHT + "[*] CAUTION: SMB service is enabled! Are you sure you want it? Also, avoid CVE-2018-7445")
|
|
||||||
else:
|
|
||||||
print(Fore.GREEN + "[+] SMB is disabled. No risk detected.")
|
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Sending a command to the router: command = `/ip smb print` - queries the status of the SMB service;
|
|
||||||
2. `output = connection.send_command(command)` - executes the command via SSH and receives its output, writing it to the variable memory;
|
|
||||||
3. If the output contains the string `“enabled: yes”`, then SMB is enabled and the script displays a warning.
|
|
||||||
|
|
||||||
The same principle works for the other checks. Only read the configuration and then analyze it in detail.
|
|
||||||
|
|
||||||
# Vulnerability Search (CVE)
|
|
||||||
|
|
||||||
Sara performs a security analysis of RouterOS by checking the current firmware version and checking it against a database of known vulnerabilities (CVEs). This process identifies critical vulnerabilities that can be exploited by attackers to compromise the device.
|
Sara performs a security analysis of RouterOS by checking the current firmware version and checking it against a database of known vulnerabilities (CVEs). This process identifies critical vulnerabilities that can be exploited by attackers to compromise the device.
|
||||||
|
|
||||||
## How does it work?
|
## How does it work?
|
||||||
|
|
||||||
Sara has a special module called `cve_analyzer.py`, which creates `routeros_cves.json` based on the NVD NIST database containing information about vulnerabilities, including those in MikroTik RouterOS.
|
Sara uses a separate module called `cve_analyzer.py`.
|
||||||
Vulnerabilities for the RouterOS version are searched for using the `--cve` argument. The results will show the total number of vulnerabilities, their categorization, as well as the CVE ID and a brief description.
|
It downloads information from the NVD (National Vulnerability Database), filters it by RouterOS, and generates a local file called `routeros_cves.json`
|
||||||
|
Next, the RouterOS version is compared with the version ranges specified in CVE records, as well as with additional patterns extracted from vulnerability descriptions.
|
||||||
|
|
||||||
|
There are two ways to perform the CVE check:
|
||||||
|
|
||||||
|
1. Live Device (SSH)
|
||||||
|
|
||||||
|
Sara will determine the RouterOS version on the device and compare it with the CVE database.
|
||||||
```bash
|
```bash
|
||||||
caster@kali:~$ sara --ip 192.168.88.1 --username admin --password admin --cve
|
~$ sara cve 192.168.88.1 admin
|
||||||
```
|
```
|
||||||
|
|
||||||
## Specifics of checking
|
If necessary, you can specify the SSH key and port:
|
||||||
|
```bash
|
||||||
|
~$ sara cve 192.168.88.1 admin ~/.ssh/id_rsa 2222
|
||||||
|
```
|
||||||
|
|
||||||
- Sara does not verify real-world exploitation of vulnerabilities. It only cross-references the RouterOS version against publicly available CVE databases;
|
> The password or key passphrase is requested interactively ([getpass](https://docs.python.org/3/library/getpass.html))
|
||||||
- If the device is running an older version of RouterOS, but vulnerable services have been manually disabled, some warnings may be false positives;
|
|
||||||
- It is recommended to manually validate your version of RouterOS after the audit to ensure there are no false positives.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
[+] Detected RouterOS Version: 7.1.1
|
[+] CVE Audit (Live)
|
||||||
|
[*] Target Device: 192.168.88.1
|
||||||
|
[*] Transport: SSH (port 22)
|
||||||
|
[?] SSH password for admin@192.168.88.1:
|
||||||
|
[✓] SSH connection established: admin@192.168.88.1
|
||||||
|
[+] Search for CVEs for a specific version
|
||||||
[!] routeros_cves.json not found.
|
[!] routeros_cves.json not found.
|
||||||
[*] Fetching CVEs from NVD...
|
[*] Fetching CVEs from NVD...
|
||||||
[+] Saved 74 CVEs to routeros_cves.json
|
[+] Saved 80 CVEs to routeros_cves.json
|
||||||
[*] Total matching CVEs: 4
|
|
||||||
[*] CRITICAL: 1
|
|
||||||
[*] HIGH: 1
|
|
||||||
[*] MEDIUM: 2
|
|
||||||
[*] Vulnerability details:
|
|
||||||
|
|
||||||
→ CVE-2022-45313 [HIGH]
|
Target RouterOS Version: 7.20.5 Matched CVEs: 0
|
||||||
Mikrotik RouterOs before stable v7.5 was discovered to contain an out-of-bounds read in the hotspot process. This vulnerability allows attackers to execute arbitrary code via a crafted nova message.
|
CRIT: 0 | HIGH: 0 | MED: 0 | LOW: 0 | UNK: 0
|
||||||
CVSS Score: 8.8
|
|
||||||
|
|
||||||
→ CVE-2022-45315 [CRITICAL]
|
[*] No known CVEs found for this RouterOS version
|
||||||
Mikrotik RouterOs before stable v7.6 was discovered to contain an out-of-bounds read in the snmp process. This vulnerability allows attackers to execute arbitrary code via a crafted packet.
|
|
||||||
CVSS Score: 9.8
|
|
||||||
|
|
||||||
→ CVE-2023-41570 [MEDIUM]
|
[*] Disconnected from RouterOS (192.168.88.1)
|
||||||
MikroTik RouterOS v7.1 to 7.11 was discovered to contain incorrect access control mechanisms in place for the Rest API.
|
|
||||||
CVSS Score: 5.3
|
|
||||||
|
|
||||||
→ CVE-2024-54772 [MEDIUM]
|
|
||||||
An issue was discovered in the Winbox service of MikroTik RouterOS long-term release v6.43.13 through v6.49.13 and stable v6.43 through v7.17.2. A patch is available in the stable release v6.49.18. A discrepancy in response size between connection attempts made with a valid username and those with an invalid username allows attackers to enumerate for valid accounts.
|
|
||||||
CVSS Score: 5.4
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> The quality of entries in the NVD leaves much to be desired; in many cases, fields such as `versionEndExcluding` or `versionStartExcluding` have a value of “null.” Therefore, it is also important to validate your RouterOS version to ensure that a particular vulnerability exists.
|
2. Manual
|
||||||
|
|
||||||
# How to use
|
If the device is unavailable, you can simply specify the desired version:
|
||||||
|
```bash
|
||||||
|
~$ sara cve version 7.13.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Sara will perform a vulnerability scan for a specific version without connecting to the device.
|
||||||
|
```bash
|
||||||
|
[+] CVE Audit (Manual Version)
|
||||||
|
[*] RouterOS Version: 7.13.1
|
||||||
|
[+] Search for CVEs for a specific version
|
||||||
|
[!] routeros_cves.json not found.
|
||||||
|
[*] Fetching CVEs from NVD...
|
||||||
|
[+] Saved 80 CVEs to routeros_cves.json
|
||||||
|
|
||||||
|
Target RouterOS Version: 7.13.1 Matched CVEs: 2
|
||||||
|
CRIT: 0 | HIGH: 1 | MED: 1 | LOW: 0 | UNK: 0
|
||||||
|
|
||||||
|
CVE ID SEV CVSS PUBLISHED
|
||||||
|
CVE-2025-6443 HIGH 7.2 2025-06-25
|
||||||
|
CVE-2024-54772 MED 5.4 2025-02-11
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features of CVE verification
|
||||||
|
|
||||||
|
Sara does not determine the possibility of actual exploitation of vulnerabilities, but only analyzes the compliance of the RouterOS version with known CVEs;
|
||||||
|
A vulnerability is considered "relevant" if the device version falls within the range specified in the CVE, or if the vulnerability description contains a recognized version range;
|
||||||
|
The NVD database often contains incomplete data (`versionStartExcluding=null`, `versionEndExcluding=null`). That's why it's also a good idea to manually check the results against MikroTik's official changelog.
|
||||||
|
|
||||||
|
# How to Use
|
||||||
|
|
||||||
You have two ways to install Sara:
|
You have two ways to install Sara:
|
||||||
|
|
||||||
|
|
@ -186,61 +146,65 @@ caster@kali:~$ sara -h
|
||||||
2. Manually using Git and Python:
|
2. Manually using Git and Python:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~$ sudo apt install git python3-colorama python3-netmiko python3-packaging
|
~$ sudo apt install git python3-colorama python3-netmiko python3-packaging python3-requests
|
||||||
~$ git clone https://github.com/caster0x00/Sara
|
~$ git clone https://github.com/caster0x00/Sara
|
||||||
~$ cd Sara
|
~$ cd Sara
|
||||||
~/Sara$ sudo python3 setup.py install
|
~/Sara$ sudo python3 setup.py install
|
||||||
~$ sara -h
|
~$ sara -h
|
||||||
```
|
```
|
||||||
|
|
||||||
## Trigger Arguments (CLI Options)
|
## Startup
|
||||||
|
|
||||||
Sara supports the following command line options:
|
The tool uses subcommands divided by purpose:
|
||||||
|
|
||||||
|
- `audit` - analyze the RouterOS configuration by profiles (system / protocols / wifi);
|
||||||
|
- `cve` - check RouterOS vulnerabilities based on CVE (live check or manually specified version)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
usage: sara.py [-h] [--ip IP] [--username USERNAME] [--password PASSWORD] [--ssh-key SSH_KEY] [--passphrase PASSPHRASE] [--port PORT] [--cve] [--skip-confirmation]
|
~$ sara <command> [...]
|
||||||
|
|
||||||
options:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
--ip IP The address of your MikroTik router
|
|
||||||
--username USERNAME SSH username (RO account can be used)
|
|
||||||
--password PASSWORD SSH password
|
|
||||||
--ssh-key SSH_KEY SSH key
|
|
||||||
--passphrase PASSPHRASE
|
|
||||||
SSH key passphrase
|
|
||||||
--port PORT SSH port (default: 22)
|
|
||||||
--cve Check RouterOS version against known CVEs
|
|
||||||
--skip-confirmation Skips legal usage confirmation prompt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
1. `--ip` - this argument specifies the IP address of the MikroTik device to which Sara is connecting;
|
## Authentication
|
||||||
|
|
||||||
2. `--username` - the SSH username that will be used to connect. Sara supports only authorized access;
|
Sara does not support passwords in command line arguments. Passwords/passphrases are requested securely via `getpass()`
|
||||||
|
|
||||||
> You can use read-only (RO) accounts. Sara does not make configuration changes, so you do not need `write` or `full` level access.
|
## Audit
|
||||||
|
|
||||||
3. `--password` - password for SSH authentication;
|
The RouterOS configuration audit is performed via SSH with selected profiles.
|
||||||
|
```bash
|
||||||
|
~$ sara audit <ip> <username> <profiles> [key] [port]
|
||||||
|
~$ sara audit 192.168.88.1 admin system
|
||||||
|
```
|
||||||
|
|
||||||
4. `--ssh-key` - specifies the ssh key that should be used to access the RouterOS's shell
|
With key (SSH):
|
||||||
|
```bash
|
||||||
|
~$ sara audit 192.168.88.1 admin system,protocols ~/.ssh/id_rsa
|
||||||
|
```
|
||||||
|
|
||||||
> This is muaually exclusive with `--password`.
|
With a non-standard port:
|
||||||
|
```bash
|
||||||
|
~$ sara audit 192.168.88.1 admin system,protocols ~/.ssh/id_rsa 2222
|
||||||
|
```
|
||||||
|
|
||||||
5. `--passphrase` - specifies the passphrase used to access the ssh-key
|
## Profiles
|
||||||
|
|
||||||
> This only works when using the `--ssh-key` argument.
|
Profiles are a comma-separated list:
|
||||||
|
|
||||||
6. `--port` - allows you to specify a non-standard SSH port for connection. The default is **22**, but if you have changed the SSH port number, it must be specified manually.
|
- `system` refers to system settings, management, users, RMI, SSH security, NAT, scheduler, etc.;
|
||||||
|
- `protocols` refers to services and network protocols (SMB, UPnP, DNS, SNMP, DDNS, Neighbor Discovery);
|
||||||
|
- `wifi` refers to security of Wi-Fi interfaces (PMKID, WPS).
|
||||||
|
|
||||||
7. `--cve` - launches a vulnerability search using the NIST NVD database.
|
You can use multiple profiles at once:
|
||||||
|
```bash
|
||||||
8. `--skip-confirmation` - allows you to skip the audit start confirmation check. Use this if you really have permission to perform a security audit.
|
system,protocols,wifi
|
||||||
|
```
|
||||||
|
|
||||||
# Copyright
|
# Copyright
|
||||||
|
|
||||||
Copyright (c) 2025 Mahama Bazarov. This project is licensed under the Apache 2.0 License
|
Copyright (c) 2026 Mahama Bazarov. This project is licensed under the Apache 2.0 License.
|
||||||
|
This project is not affiliated with or endorsed by SIA Mikrotīkls
|
||||||
|
All MikroTik trademarks and product names are the property of their respective owners.
|
||||||
|
|
||||||
# Outro
|
# Outro
|
||||||
|
|
||||||
MikroTik devices are widely used around the world. Sara is designed to help engineers improve security - use it wisely.
|
If you have any suggestions or find any bugs, feel free to create issues in the repository or contact me: [mahamabazarov@mailbox.org](mailto:mahamabazarov@mailbox.org)
|
||||||
|
|
||||||
E-mail for contact: mahamabazarov@mailbox.org
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
BIN
cover/saracover.png
Normal file
BIN
cover/saracover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 MiB |
405
cve_analyzer.py
405
cve_analyzer.py
|
|
@ -1,39 +1,99 @@
|
||||||
# Auxiliary module cve_analyzer.py for searching CVE using the NIST NVD database
|
# Auxiliary module cve_analyzer.py for searching CVE using the NIST NVD database
|
||||||
|
#
|
||||||
# Copyright (c) 2025 Mahama Bazarov
|
# Copyright (c) 2026 Mahama Bazarov
|
||||||
# Licensed under the Apache 2.0 License
|
# Licensed under the Apache 2.0 License
|
||||||
# This project is not affiliated with or endorsed by MikroTik
|
# This project is not affiliated with or endorsed by SIA Mikrotīkls
|
||||||
|
|
||||||
import json, re, os, requests, time
|
import json, re, os, requests, time
|
||||||
from packaging.version import Version, InvalidVersion
|
from packaging.version import Version, InvalidVersion
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
|
|
||||||
# Constants
|
# NVD v2.0 URL and settings
|
||||||
NVD_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
NVD_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
||||||
KEYWORD = "routeros"
|
KEYWORD = "routeros"
|
||||||
RESULTS_PER_PAGE = 2000
|
RESULTS_PER_PAGE = 2000
|
||||||
OUTPUT_FILE = "routeros_cves.json"
|
OUTPUT_FILE = "routeros_cves.json"
|
||||||
|
GUTTER = 2 # spaces between columns
|
||||||
|
|
||||||
# Converts version string to a comparable Version object
|
|
||||||
|
# Simple role-based color mapping
|
||||||
|
def paint(role: str, text: str) -> str:
|
||||||
|
role_value = (role or "").lower()
|
||||||
|
|
||||||
|
if role_value in ("crit", "fail"):
|
||||||
|
return Fore.RED + text + Style.RESET_ALL
|
||||||
|
if role_value == "warn":
|
||||||
|
return Fore.YELLOW + text + Style.RESET_ALL
|
||||||
|
if role_value == "ok":
|
||||||
|
return Fore.GREEN + text + Style.RESET_ALL
|
||||||
|
if role_value == "info":
|
||||||
|
return Fore.CYAN + text + Style.RESET_ALL
|
||||||
|
if role_value == "label":
|
||||||
|
return Fore.CYAN + text + Style.RESET_ALL
|
||||||
|
if role_value == "value":
|
||||||
|
return Style.BRIGHT + text + Style.RESET_ALL
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# ANSI and OSC-8 stripping for width calculation
|
||||||
|
ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
||||||
|
OSC8_BEL = re.compile(r"\x1b]8;;.*?\x07")
|
||||||
|
OSC8_ST = re.compile(r"\x1b]8;;.*?\x1b\\")
|
||||||
|
|
||||||
|
|
||||||
|
def strip_osc8(s: str) -> str:
|
||||||
|
tmp = OSC8_BEL.sub("", s)
|
||||||
|
tmp = OSC8_ST.sub("", tmp)
|
||||||
|
return tmp
|
||||||
|
|
||||||
|
|
||||||
|
def visible_len(s: str) -> int:
|
||||||
|
raw = strip_osc8(s)
|
||||||
|
no_ansi = ANSI_RE.sub("", raw)
|
||||||
|
return len(no_ansi)
|
||||||
|
|
||||||
|
|
||||||
|
def pad_r(s: str, width: int) -> str:
|
||||||
|
length = visible_len(s)
|
||||||
|
pad_len = width - length
|
||||||
|
if pad_len < 0:
|
||||||
|
pad_len = 0
|
||||||
|
return s + " " * pad_len
|
||||||
|
|
||||||
|
|
||||||
|
def pad_l(s: str, width: int) -> str:
|
||||||
|
length = visible_len(s)
|
||||||
|
pad_len = width - length
|
||||||
|
if pad_len < 0:
|
||||||
|
pad_len = 0
|
||||||
|
return " " * pad_len + s
|
||||||
|
|
||||||
|
|
||||||
|
# Clickable hyperlink (OSC-8)
|
||||||
|
def term_link(text: str, url: str) -> str:
|
||||||
|
return f"\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\"
|
||||||
|
|
||||||
|
|
||||||
|
# Convert RouterOS version string to Version object
|
||||||
def normalize_version(v):
|
def normalize_version(v):
|
||||||
if not v:
|
if not v:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return Version(v)
|
return Version(v)
|
||||||
except InvalidVersion:
|
except InvalidVersion:
|
||||||
# Strip unstable labels like 'rc', 'beta', etc
|
cleaned = re.sub(r"(rc|beta|testing|stable)[\d\-]*", "", v, flags=re.IGNORECASE)
|
||||||
cleaned = re.sub(r'(rc|beta|testing|stable)[\d\-]*', '', v, flags=re.IGNORECASE)
|
|
||||||
try:
|
try:
|
||||||
return Version(cleaned)
|
return Version(cleaned)
|
||||||
except InvalidVersion:
|
except InvalidVersion:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Extract version ranges from CVE descriptions
|
|
||||||
|
# Extract version ranges from CVE description text
|
||||||
def extract_ranges_from_description(description):
|
def extract_ranges_from_description(description):
|
||||||
description = description.lower()
|
description = (description or "").lower()
|
||||||
ranges = []
|
ranges = []
|
||||||
|
|
||||||
# Match version ranges in various formats
|
|
||||||
matches = re.findall(r"(?:from\s+)?v?(\d+\.\d+(?:\.\d+)?)\s+to\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
matches = re.findall(r"(?:from\s+)?v?(\d+\.\d+(?:\.\d+)?)\s+to\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
||||||
for start, end in matches:
|
for start, end in matches:
|
||||||
ranges.append({"versionStartIncluding": start, "versionEndIncluding": end})
|
ranges.append({"versionStartIncluding": start, "versionEndIncluding": end})
|
||||||
|
|
@ -52,7 +112,7 @@ def extract_ranges_from_description(description):
|
||||||
|
|
||||||
matches = re.findall(r"v?(\d+\.\d+)\.x", description)
|
matches = re.findall(r"v?(\d+\.\d+)\.x", description)
|
||||||
for base in matches:
|
for base in matches:
|
||||||
ranges.append({"versionEndIncluding": f"{base}.999"})
|
ranges.append({"versionStartIncluding": f"{base}.0", "versionEndIncluding": f"{base}.999"})
|
||||||
|
|
||||||
matches = re.findall(r"(?:up to|and below)\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
matches = re.findall(r"(?:up to|and below)\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
||||||
for end in matches:
|
for end in matches:
|
||||||
|
|
@ -60,41 +120,43 @@ def extract_ranges_from_description(description):
|
||||||
|
|
||||||
return ranges
|
return ranges
|
||||||
|
|
||||||
# Determines if the given version falls within a vulnerable range
|
|
||||||
|
# Check if current version falls into a vulnerable range
|
||||||
def is_version_affected(current_v, version_info):
|
def is_version_affected(current_v, version_info):
|
||||||
def get(v_key):
|
def get(v_key):
|
||||||
return normalize_version(version_info.get(v_key))
|
return normalize_version(version_info.get(v_key))
|
||||||
|
|
||||||
criteria = version_info.get("criteria", "")
|
criteria_raw = version_info.get("criteria", "") or ""
|
||||||
|
criteria = criteria_raw.lower()
|
||||||
end_excl_raw = version_info.get("versionEndExcluding", "")
|
end_excl_raw = version_info.get("versionEndExcluding", "")
|
||||||
|
|
||||||
# RouterOS 6.x vs 7.x false positive prevention
|
if criteria and ("mikrotik" not in criteria or "routeros" not in criteria):
|
||||||
|
return False
|
||||||
|
|
||||||
if isinstance(end_excl_raw, str) and end_excl_raw.startswith("7") and str(current_v).startswith("6."):
|
if isinstance(end_excl_raw, str) and end_excl_raw.startswith("7") and str(current_v).startswith("6."):
|
||||||
return False
|
return False
|
||||||
if isinstance(end_excl_raw, str) and end_excl_raw.startswith("6") and str(current_v).startswith("7."):
|
if isinstance(end_excl_raw, str) and end_excl_raw.startswith("6") and str(current_v).startswith("7."):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Parse version range keys
|
|
||||||
start_incl = get("versionStartIncluding")
|
start_incl = get("versionStartIncluding")
|
||||||
start_excl = get("versionStartExcluding")
|
start_excl = get("versionStartExcluding")
|
||||||
end_incl = get("versionEndIncluding")
|
end_incl = get("versionEndIncluding")
|
||||||
end_excl = get("versionEndExcluding")
|
end_excl = get("versionEndExcluding")
|
||||||
|
|
||||||
# Fallback: match exact version in criteria if no range info is provided
|
|
||||||
if not any([start_incl, start_excl, end_incl, end_excl]):
|
if not any([start_incl, start_excl, end_incl, end_excl]):
|
||||||
version_match = re.search(r"routeros:([\w.\-]+)", criteria)
|
version_match = re.search(r"routeros:([\w.\-]+)", criteria_raw)
|
||||||
if version_match:
|
if version_match:
|
||||||
version_exact = normalize_version(version_match.group(1))
|
version_exact = normalize_version(version_match.group(1))
|
||||||
return version_exact is not None and current_v == version_exact
|
return version_exact is not None and current_v == version_exact
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Skip if range is invalid or unparseable
|
for raw_key, normed in zip(
|
||||||
for raw, normed in zip(["versionStartIncluding", "versionStartExcluding", "versionEndIncluding", "versionEndExcluding"],
|
["versionStartIncluding", "versionStartExcluding", "versionEndIncluding", "versionEndExcluding"],
|
||||||
[start_incl, start_excl, end_incl, end_excl]):
|
[start_incl, start_excl, end_incl, end_excl],
|
||||||
if version_info.get(raw) and normed is None:
|
):
|
||||||
|
if version_info.get(raw_key) and normed is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Perform actual version comparisons
|
|
||||||
if start_incl and current_v < start_incl:
|
if start_incl and current_v < start_incl:
|
||||||
return False
|
return False
|
||||||
if start_excl and current_v <= start_excl:
|
if start_excl and current_v <= start_excl:
|
||||||
|
|
@ -106,7 +168,8 @@ def is_version_affected(current_v, version_info):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Downloads and stores all CVEs from NVD
|
|
||||||
|
# Download all RouterOS CVEs from NVD and save locally
|
||||||
def fetch_all_cves():
|
def fetch_all_cves():
|
||||||
all_cves = []
|
all_cves = []
|
||||||
start_index = 0
|
start_index = 0
|
||||||
|
|
@ -116,7 +179,7 @@ def fetch_all_cves():
|
||||||
params = {
|
params = {
|
||||||
"keywordSearch": KEYWORD,
|
"keywordSearch": KEYWORD,
|
||||||
"startIndex": start_index,
|
"startIndex": start_index,
|
||||||
"resultsPerPage": RESULTS_PER_PAGE
|
"resultsPerPage": RESULTS_PER_PAGE,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
response = requests.get(NVD_URL, params=params, timeout=30)
|
response = requests.get(NVD_URL, params=params, timeout=30)
|
||||||
|
|
@ -132,15 +195,14 @@ def fetch_all_cves():
|
||||||
cve_items = data.get("vulnerabilities", [])
|
cve_items = data.get("vulnerabilities", [])
|
||||||
total_results = data.get("totalResults", 0)
|
total_results = data.get("totalResults", 0)
|
||||||
|
|
||||||
# Process each CVE entry
|
|
||||||
for item in cve_items:
|
for item in cve_items:
|
||||||
cve = item.get("cve", {})
|
cve = item.get("cve", {})
|
||||||
cve_id = cve.get("id")
|
cve_id = cve.get("id")
|
||||||
description = next((d["value"] for d in cve.get("descriptions", []) if d["lang"] == "en"), "")
|
description = next((d["value"] for d in cve.get("descriptions", []) if d.get("lang") == "en"), "")
|
||||||
severity = "UNKNOWN"
|
severity = "UNKNOWN"
|
||||||
score = "N/A"
|
score = "N/A"
|
||||||
|
published = cve.get("published", "")
|
||||||
|
|
||||||
# Extract CVSS score/severity
|
|
||||||
metrics = cve.get("metrics", {})
|
metrics = cve.get("metrics", {})
|
||||||
if "cvssMetricV31" in metrics:
|
if "cvssMetricV31" in metrics:
|
||||||
cvss = metrics["cvssMetricV31"][0]["cvssData"]
|
cvss = metrics["cvssMetricV31"][0]["cvssData"]
|
||||||
|
|
@ -151,62 +213,49 @@ def fetch_all_cves():
|
||||||
severity = cvss.get("baseSeverity", "UNKNOWN")
|
severity = cvss.get("baseSeverity", "UNKNOWN")
|
||||||
score = cvss.get("baseScore", "N/A")
|
score = cvss.get("baseScore", "N/A")
|
||||||
|
|
||||||
# Extract affected version ranges
|
|
||||||
affected_versions = []
|
affected_versions = []
|
||||||
for config in cve.get("configurations", []):
|
for config in cve.get("configurations", []):
|
||||||
for node in config.get("nodes", []):
|
for node in config.get("nodes", []):
|
||||||
for match in node.get("cpeMatch", []):
|
for match in node.get("cpeMatch", []):
|
||||||
affected_versions.append({
|
if not match.get("vulnerable", False):
|
||||||
"criteria": match.get("criteria"),
|
continue
|
||||||
|
criteria = match.get("criteria", "") or ""
|
||||||
|
crit_l = criteria.lower()
|
||||||
|
if "mikrotik" not in crit_l or "routeros" not in crit_l:
|
||||||
|
continue
|
||||||
|
affected_versions.append(
|
||||||
|
{
|
||||||
|
"criteria": criteria,
|
||||||
"versionStartIncluding": match.get("versionStartIncluding"),
|
"versionStartIncluding": match.get("versionStartIncluding"),
|
||||||
"versionStartExcluding": match.get("versionStartExcluding"),
|
"versionStartExcluding": match.get("versionStartExcluding"),
|
||||||
"versionEndIncluding": match.get("versionEndIncluding"),
|
"versionEndIncluding": match.get("versionEndIncluding"),
|
||||||
"versionEndExcluding": match.get("versionEndExcluding")
|
"versionEndExcluding": match.get("versionEndExcluding"),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
all_cves.append({
|
all_cves.append(
|
||||||
|
{
|
||||||
"cve_id": cve_id,
|
"cve_id": cve_id,
|
||||||
"description": description,
|
"description": description,
|
||||||
"severity": severity,
|
"severity": severity,
|
||||||
"cvss_score": score,
|
"cvss_score": score,
|
||||||
"affected_versions": affected_versions
|
"published": published,
|
||||||
})
|
"affected_versions": affected_versions,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
start_index += RESULTS_PER_PAGE
|
start_index += RESULTS_PER_PAGE
|
||||||
if start_index >= total_results:
|
if start_index >= total_results:
|
||||||
break
|
break
|
||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
|
|
||||||
# Write results to file
|
|
||||||
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(all_cves, f, indent=2, ensure_ascii=False)
|
json.dump(all_cves, f, indent=2, ensure_ascii=False)
|
||||||
print(Fore.GREEN + f"[+] Saved {len(all_cves)} CVEs to {OUTPUT_FILE}")
|
print(Fore.GREEN + f"[+] Saved {len(all_cves)} CVEs to {OUTPUT_FILE}")
|
||||||
|
|
||||||
# Perform local audit of current RouterOS version against cached CVE data
|
|
||||||
def run_cve_audit(connection):
|
|
||||||
# Banner
|
|
||||||
print(Fore.WHITE + "=" * 60)
|
|
||||||
print(Fore.WHITE + "[!] Checking CVE Vulnerabilities")
|
|
||||||
print(Fore.MAGENTA + "[!] In any case, validate results manually due to potential false positives.")
|
|
||||||
print(Fore.WHITE + "=" * 60)
|
|
||||||
|
|
||||||
# Retrieve RouterOS version from device
|
# Load local CVE cache and optionally refresh it
|
||||||
output = connection.send_command("/system resource print")
|
def load_cve_data():
|
||||||
match = re.search(r"version:\s*([\w.\-]+)", output)
|
|
||||||
if not match:
|
|
||||||
print(Fore.RED + "[-] ERROR: Could not determine RouterOS version.")
|
|
||||||
return
|
|
||||||
|
|
||||||
current_version = match.group(1)
|
|
||||||
current_v = normalize_version(current_version)
|
|
||||||
|
|
||||||
if not current_v:
|
|
||||||
print(Fore.RED + f"[-] ERROR: RouterOS version '{current_version}' is invalid.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(Fore.GREEN + f"[+] Detected RouterOS Version: {current_version}")
|
|
||||||
|
|
||||||
# Load or refresh CVE data
|
|
||||||
if not os.path.isfile(OUTPUT_FILE):
|
if not os.path.isfile(OUTPUT_FILE):
|
||||||
print(Fore.YELLOW + f"[!] {OUTPUT_FILE} not found.")
|
print(Fore.YELLOW + f"[!] {OUTPUT_FILE} not found.")
|
||||||
fetch_all_cves()
|
fetch_all_cves()
|
||||||
|
|
@ -216,23 +265,161 @@ def run_cve_audit(connection):
|
||||||
if answer == "yes":
|
if answer == "yes":
|
||||||
fetch_all_cves()
|
fetch_all_cves()
|
||||||
|
|
||||||
# Load local CVE file
|
|
||||||
try:
|
try:
|
||||||
with open(OUTPUT_FILE, "r", encoding="utf-8") as f:
|
with open(OUTPUT_FILE, "r", encoding="utf-8") as f:
|
||||||
cve_data = json.load(f)
|
return json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(Fore.RED + f"[-] Failed to load {OUTPUT_FILE}: {e}")
|
print(Fore.RED + f"[-] Failed to load {OUTPUT_FILE}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Format CVSS score as 0.1 or N/A
|
||||||
|
def fmt_cvss(x):
|
||||||
|
try:
|
||||||
|
value = float(x)
|
||||||
|
return f"{value:0.1f}"
|
||||||
|
except Exception:
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
# Severity rank for sorting
|
||||||
|
sev_rank = {
|
||||||
|
"CRITICAL": 0,
|
||||||
|
"HIGH": 1,
|
||||||
|
"MEDIUM": 2,
|
||||||
|
"LOW": 3,
|
||||||
|
"UNKNOWN": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def key_tuple(m):
|
||||||
|
sev = m.get("severity", "UNKNOWN") or "UNKNOWN"
|
||||||
|
sev_u = sev.upper()
|
||||||
|
rank = sev_rank.get(sev_u, 4)
|
||||||
|
pub = m.get("published") or ""
|
||||||
|
return (rank, pub)
|
||||||
|
|
||||||
|
|
||||||
|
# Severity tag (CRIT/HIGH/MED/LOW/UNK)
|
||||||
|
def sev_tag(sev: str):
|
||||||
|
sev_u = (sev or "UNKNOWN").upper()
|
||||||
|
if sev_u == "CRITICAL":
|
||||||
|
return paint("crit", "CRIT")
|
||||||
|
if sev_u == "HIGH":
|
||||||
|
return paint("fail", "HIGH")
|
||||||
|
if sev_u == "MEDIUM":
|
||||||
|
return paint("warn", "MED")
|
||||||
|
if sev_u == "LOW":
|
||||||
|
return paint("info", "LOW")
|
||||||
|
return paint("value", "UNK")
|
||||||
|
|
||||||
|
|
||||||
|
def count_seg(label: str, n: int):
|
||||||
|
role_map = {
|
||||||
|
"CRIT": "crit",
|
||||||
|
"HIGH": "fail",
|
||||||
|
"MED": "warn",
|
||||||
|
"LOW": "info",
|
||||||
|
"UNK": "value",
|
||||||
|
}
|
||||||
|
role = role_map[label]
|
||||||
|
return paint(role, label) + ": " + paint("value", str(n))
|
||||||
|
|
||||||
|
|
||||||
|
# Summary header
|
||||||
|
def render_summary(version: str, matches, counters):
|
||||||
|
c_crit = counters.get("CRITICAL", 0)
|
||||||
|
c_high = counters.get("HIGH", 0)
|
||||||
|
c_med = counters.get("MEDIUM", 0)
|
||||||
|
c_low = counters.get("LOW", 0)
|
||||||
|
c_unk = counters.get("UNKNOWN", 0)
|
||||||
|
|
||||||
|
line1 = (
|
||||||
|
paint("label", "Target RouterOS Version:") + " " + paint("value", version)
|
||||||
|
+ " "
|
||||||
|
+ paint("label", "Matched CVEs:") + " " + paint("value", str(len(matches)))
|
||||||
|
)
|
||||||
|
|
||||||
|
parts = [
|
||||||
|
count_seg("CRIT", c_crit),
|
||||||
|
count_seg("HIGH", c_high),
|
||||||
|
count_seg("MED", c_med),
|
||||||
|
count_seg("LOW", c_low),
|
||||||
|
count_seg("UNK", c_unk),
|
||||||
|
]
|
||||||
|
line2 = " | ".join(parts)
|
||||||
|
|
||||||
|
print(line1)
|
||||||
|
print(line2)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# Table rendering
|
||||||
|
def render_cve(rows):
|
||||||
|
max_id_len = 14
|
||||||
|
for m in rows:
|
||||||
|
cve_id = m.get("cve_id", "") or ""
|
||||||
|
length = len(cve_id)
|
||||||
|
if length > max_id_len:
|
||||||
|
max_id_len = length
|
||||||
|
|
||||||
|
id_w = max_id_len + 2
|
||||||
|
if id_w < 16:
|
||||||
|
id_w = 16
|
||||||
|
if id_w > 22:
|
||||||
|
id_w = 22
|
||||||
|
|
||||||
|
sev_w = 5
|
||||||
|
cvss_w = 4
|
||||||
|
pub_w = 10
|
||||||
|
sep = " " * GUTTER
|
||||||
|
|
||||||
|
head = (
|
||||||
|
pad_r(paint("label", "CVE ID"), id_w)
|
||||||
|
+ sep
|
||||||
|
+ pad_r(paint("label", "SEV"), sev_w)
|
||||||
|
+ sep
|
||||||
|
+ pad_r(paint("label", "CVSS"), cvss_w)
|
||||||
|
+ sep
|
||||||
|
+ pad_r(paint("label", "PUBLISHED"), pub_w)
|
||||||
|
)
|
||||||
|
print(head)
|
||||||
|
|
||||||
|
for m in rows:
|
||||||
|
cve_id = m.get("cve_id", "") or ""
|
||||||
|
link = term_link(cve_id, "https://nvd.nist.gov/vuln/detail/" + cve_id)
|
||||||
|
|
||||||
|
sev_t = sev_tag(m.get("severity", "UNKNOWN"))
|
||||||
|
cvss = fmt_cvss(m.get("cvss_score", "N/A"))
|
||||||
|
|
||||||
|
published = m.get("published") or "-"
|
||||||
|
published = str(published)[:10]
|
||||||
|
|
||||||
|
line = (
|
||||||
|
pad_r(paint("info", link), id_w)
|
||||||
|
+ sep
|
||||||
|
+ pad_r(sev_t, sev_w)
|
||||||
|
+ sep
|
||||||
|
+ pad_l(paint("value", cvss), cvss_w)
|
||||||
|
+ sep
|
||||||
|
+ pad_r(paint("value", published), pub_w)
|
||||||
|
)
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
# Core CVE matching logic for a given RouterOS version
|
||||||
|
def run_cve_match_for_version(current_v, current_version: str):
|
||||||
|
cve_data = load_cve_data()
|
||||||
|
if not cve_data:
|
||||||
return
|
return
|
||||||
|
|
||||||
# CVE match logic
|
|
||||||
counters = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0}
|
counters = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0}
|
||||||
hits = []
|
matches = []
|
||||||
|
|
||||||
for cve in cve_data:
|
for cve in cve_data:
|
||||||
matched = False
|
matched = False
|
||||||
affected_versions = cve.get("affected_versions", [])
|
affected_versions = cve.get("affected_versions", [])
|
||||||
|
|
||||||
# Fallback: try parsing version from description if structured data is missing
|
|
||||||
if not affected_versions:
|
if not affected_versions:
|
||||||
affected_versions = extract_ranges_from_description(cve.get("description", ""))
|
affected_versions = extract_ranges_from_description(cve.get("description", ""))
|
||||||
|
|
||||||
|
|
@ -241,41 +428,59 @@ def run_cve_audit(connection):
|
||||||
matched = True
|
matched = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if matched:
|
if not matched:
|
||||||
hits.append(cve)
|
continue
|
||||||
severity = cve.get("severity", "UNKNOWN").upper()
|
|
||||||
counters[severity] = counters.get(severity, 0) + 1
|
|
||||||
|
|
||||||
# Display summary
|
sev = (cve.get("severity", "UNKNOWN") or "UNKNOWN").upper()
|
||||||
total = len(hits)
|
counters[sev] = counters.get(sev, 0) + 1
|
||||||
print(Fore.WHITE + f"[*] Total matching CVEs: {total}")
|
|
||||||
for level in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"]:
|
|
||||||
count = counters.get(level, 0)
|
|
||||||
if count > 0:
|
|
||||||
color = {
|
|
||||||
"CRITICAL": Fore.RED + Style.BRIGHT,
|
|
||||||
"HIGH": Fore.RED,
|
|
||||||
"MEDIUM": Fore.YELLOW,
|
|
||||||
"LOW": Fore.CYAN,
|
|
||||||
"UNKNOWN": Fore.WHITE
|
|
||||||
}[level]
|
|
||||||
print(color + f"[*] {level}: {count}")
|
|
||||||
|
|
||||||
# Print vulnerability details
|
matches.append(
|
||||||
if total > 0:
|
{
|
||||||
print(Fore.WHITE + "[*] Vulnerability details:")
|
"cve_id": cve.get("cve_id", ""),
|
||||||
for cve in hits:
|
"severity": sev,
|
||||||
severity = cve.get("severity", "UNKNOWN").upper()
|
"cvss_score": cve.get("cvss_score", "N/A"),
|
||||||
description = cve.get("description", "").strip()
|
"published": cve.get("published", ""),
|
||||||
score = cve.get("cvss_score", "N/A")
|
}
|
||||||
color = {
|
)
|
||||||
"CRITICAL": Fore.RED + Style.BRIGHT,
|
|
||||||
"HIGH": Fore.RED,
|
|
||||||
"MEDIUM": Fore.YELLOW,
|
|
||||||
"LOW": Fore.CYAN,
|
|
||||||
"UNKNOWN": Fore.WHITE
|
|
||||||
}.get(severity, Fore.WHITE)
|
|
||||||
|
|
||||||
print(color + f"\n→ {cve['cve_id']} [{severity}]")
|
print()
|
||||||
print(Fore.WHITE + " " + description)
|
render_summary(current_version, matches, counters)
|
||||||
print(Fore.WHITE + f" CVSS Score: {score}")
|
|
||||||
|
if not matches:
|
||||||
|
print(paint("ok", "[*] No known CVEs found for this RouterOS version"))
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
rows = sorted(matches, key=key_tuple)
|
||||||
|
render_cve(rows)
|
||||||
|
|
||||||
|
|
||||||
|
# Live CVE audit based on device version (kept for potential reuse)
|
||||||
|
def run_cve_audit(connection):
|
||||||
|
print(paint("label", "[+] Search for CVEs for a specific version"))
|
||||||
|
output = connection.send_command("/system resource print")
|
||||||
|
match = re.search(r"version:\s*([\w.\-]+)", output)
|
||||||
|
if not match:
|
||||||
|
print(Fore.RED + "[-] ERROR: Could not determine RouterOS version.")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_version = match.group(1)
|
||||||
|
current_v = normalize_version(current_version)
|
||||||
|
if not current_v:
|
||||||
|
print(Fore.RED + f"[-] ERROR: RouterOS version '{current_version}' is invalid.")
|
||||||
|
return
|
||||||
|
|
||||||
|
run_cve_match_for_version(current_v, current_version)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# CVE audit for manually provided RouterOS version string (used by Sara)
|
||||||
|
def run_cve_audit_for_version(version_str: str):
|
||||||
|
print(paint("label", "[+] Search for CVEs for a specific version"))
|
||||||
|
current_version = version_str.strip()
|
||||||
|
current_v = normalize_version(current_version)
|
||||||
|
if not current_v:
|
||||||
|
print(Fore.RED + f"[-] ERROR: RouterOS version '{current_version}' is invalid.")
|
||||||
|
return
|
||||||
|
|
||||||
|
run_cve_match_for_version(current_v, current_version)
|
||||||
|
|
|
||||||
5
setup.py
5
setup.py
|
|
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="sara",
|
name="sara",
|
||||||
version="1.2",
|
version="1.3",
|
||||||
url="https://github.com/caster0x00/Sara",
|
url="https://github.com/caster0x00/Sara",
|
||||||
author="Mahama Bazarov",
|
author="Mahama Bazarov",
|
||||||
author_email="mahamabazarov@mailbox.org",
|
author_email="mahamabazarov@mailbox.org",
|
||||||
|
|
@ -11,12 +11,13 @@ setup(
|
||||||
long_description=open('README.md', encoding="utf8").read(),
|
long_description=open('README.md', encoding="utf8").read(),
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
license="Apache-2.0",
|
license="Apache-2.0",
|
||||||
keywords=['mikrotik', 'routeros', 'config analyzer', 'network security',],
|
keywords=['mikrotik', 'routeros', 'config analyzer', 'network security', 'cve',],
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'colorama',
|
'colorama',
|
||||||
'netmiko',
|
'netmiko',
|
||||||
'packaging',
|
'packaging',
|
||||||
|
'requests',
|
||||||
],
|
],
|
||||||
py_modules=['cve_analyzer'],
|
py_modules=['cve_analyzer'],
|
||||||
entry_points={
|
entry_points={
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue