Sara/sara.py
caster0x00 fc94270c0e v1.3
2025-12-09 16:30:21 +04:00

886 lines
No EOL
30 KiB
Python

#!/usr/bin/env python3
# Sara: MikroTik RouterOS Security Inspector
# Copyright (c) 2026 Mahama Bazarov
# Licensed under the Apache 2.0 License
# This project is not affiliated with or endorsed by SIA Mikrotīkls
import argparse
import colorama
import re
import sys
import os
from getpass import getpass
from netmiko import ConnectHandler
from colorama import Fore, Style
from packaging.version import Version
from cve_analyzer import run_cve_audit, run_cve_audit_for_version
# init colors
colorama.init(autoreset=True)
INDENT = " "
# print banner
def banner():
banner_art = r"""
_____
/ ___/____ __________ _
\__ \/ __ `/ ___/ __ `/
___/ / /_/ / / / /_/ /
/____/\__,_/_/ \__,_/
"""
print(INDENT + banner_art)
print(INDENT + "Sara: " + Style.RESET_ALL + "MikroTik RouterOS Security Inspector")
print(INDENT + "Developer: " + Style.RESET_ALL + "Mahama Bazarov (Caster)")
print(INDENT + "Contact: " + Style.RESET_ALL + "mahamabazarov@mailbox.org")
print(INDENT + "Version: " + Style.RESET_ALL + "1.3.0")
print(INDENT + "Documentation & Usage: " + Style.RESET_ALL + "https://github.com/caster0x00/Sara")
# section header
def section(title: str):
print()
print(Fore.WHITE + f"[+] {title}" + Style.RESET_ALL)
# info line
def info(msg: str):
print(Fore.WHITE + INDENT + f"[*] {msg}")
# ok line
def ok(msg: str):
print(Fore.GREEN + INDENT + f"[✓] {msg}")
# warning line
def warn(msg: str):
print(Fore.YELLOW + INDENT + f"[!] {msg}")
# high severity line
def alert(msg: str):
print(Fore.RED + INDENT + f"[!] {msg}")
# error line
def error(msg: str):
print(Fore.RED + INDENT + f"[-] {msg}")
# detailed line
def detail(msg: str):
print(Fore.LIGHTWHITE_EX + INDENT * 2 + f"[*] {msg}" + Style.RESET_ALL)
# ssh connection helper
def connect_to_router(ip, user, password=None, port=22, key_file=None, key_passphrase=None):
device = {
"device_type": "mikrotik_routeros",
"host": ip,
"username": user,
"port": port,
}
# key-based auth
if key_file:
key_path = os.path.expanduser(key_file)
if not os.path.exists(key_path):
error(f"SSH key not found: {key_path}")
info("Provide path to the private key (not .pub)")
sys.exit(1)
device["use_keys"] = True
device["key_file"] = key_path
if key_passphrase:
device["passphrase"] = key_passphrase
else:
# password auth
if not password:
error("No authentication method provided")
sys.exit(1)
device["password"] = password
device["use_keys"] = False
# connect
try:
conn = ConnectHandler(**device)
ok(f"SSH connection established: {user}@{ip}")
return conn
except Exception as e:
error(f"SSH connection failed: {e}")
sys.exit(1)
# resolve auth method and prompt
def normalize_auth_and_prompt(args):
key_file = args.key
key_passphrase = None
# key flow
if key_file:
key_file = os.path.expanduser(key_file)
if not os.path.exists(key_file):
error(f"SSH key file not found: {key_file}")
info("Provide path to the private key (not .pub)")
sys.exit(1)
prompt = f"[?] Passphrase for key {key_file} (leave empty if none): "
entered = getpass(prompt)
if entered:
key_passphrase = entered
return None, key_file, key_passphrase
# password flow
password = getpass(f"[?] SSH password for {args.username}@{args.ip}: ")
return password, None, None
# simple version wrapper
def parse_version(version_str):
return Version(version_str)
# detect and print RouterOS version
def check_routeros_version(connection):
# run resource print
output = connection.send_command("/system resource print")
match = re.search(r"version:\s*([\d.]+)", output)
if match:
routeros_version = parse_version(match.group(1))
info(f"Detected RouterOS: {Fore.MAGENTA}{routeros_version}{Style.RESET_ALL}")
else:
error("Could not determine RouterOS version")
# SMB service check
def check_smb(connection):
section("SMB Service")
output = connection.send_command("/ip smb print")
if "enabled: yes" in output:
alert("SMB service is enabled! Do you need SMB? Also avoid CVE-2018-7445")
else:
ok("SMB is disabled")
ok("No issues detected")
# RMI services exposure check
def check_rmi_services(connection):
section("Remote Management (RMI/MGMT)")
output = connection.send_command("/ip service print")
high_risk = ["telnet", "ftp", "www"]
moderate_risk = ["api", "api-ssl", "winbox", "www-ssl"]
ssh = ["ssh"]
risks_found = False
# scan line by line
for line in output.splitlines():
line = line.strip()
if not line:
continue
# skip disabled/default
if re.search(r"^\d+\s+X\b", line):
continue
if re.search(r"^\d+\s+D\b", line):
continue
match = re.search(r"(\S+)\s+\d+", line)
if not match:
continue
service_name = match.group(1).lower()
display_name = service_name.upper().replace("WWW", "HTTP").replace("WWW-SSL", "HTTPS")
# high risk
if service_name in high_risk:
alert(f"{display_name} is enabled (high risk)")
if service_name == "ftp":
warn("FTP transmits credentials in cleartext")
if service_name == "telnet":
warn("Telnet allows credential interception")
if service_name == "www":
warn("HTTP credentials can be sniffed over the network")
risks_found = True
continue
# medium risk
if service_name in moderate_risk:
warn(f"{display_name} is enabled")
if service_name in ["api", "api-ssl"]:
info("RouterOS API is a brute-force target; restrict access")
elif service_name == "www-ssl":
info("Ensure HTTPS uses strong ciphers and valid certificates")
elif service_name == "winbox":
warn("Winbox enabled. Winbox 'Keep Password' may store credentials in plaintext. If the PC is compromised, saved passwords may be extracted!")
continue
# ssh
if service_name in ssh:
ok(f"{display_name} enabled. Use strong passwords or SSH keys for authentication")
if not risks_found:
ok("No high-risk RMI services detected")
# default usernames check
def check_default_users(connection):
section("Default Usernames")
output = connection.send_command("/user print detail")
default_users = {"admin", "engineer", "user", "test", "root", "mikrotik", "routeros"}
risks_found = False
# split user blocks
for block in output.split("\n\n"):
match = re.search(r'name="([^"]+)"', block)
if not match:
continue
username = match.group(1).lower()
if username in default_users:
warn(f"Default username detected: '{username}'")
info("Change it to a unique value to reduce attack surface")
risks_found = True
if not risks_found:
ok("No default usernames found")
# check service address-list on /ip service
def checking_access_to_RMI(connection):
section("RMI/MGMT Access Restrictions")
output = connection.send_command("/ip service print")
lines = output.splitlines()
header_line = None
# find header
for line in lines:
if line.strip().startswith("#"):
header_line = line
break
if not header_line:
error("Unable to parse /ip service print header")
return
try:
idx_name = header_line.index("NAME")
idx_addr = header_line.index("ADDRESS")
idx_cert = header_line.index("CERTIFICATE") if "CERTIFICATE" in header_line else None
except ValueError:
error("Expected columns NAME/ADDRESS not found in /ip service print output")
return
risks_found = False
# parse body
for line in lines:
if not line.strip():
continue
if line == header_line or line.strip().startswith("Flags:") or line.strip().startswith("Columns:"):
continue
stripped = line.lstrip()
if not stripped or not stripped[0].isdigit():
continue
flags_field = line[:idx_name]
if "X" in flags_field or "D" in flags_field:
continue
service_name_raw = line[idx_name:idx_addr].strip()
if not service_name_raw:
continue
service_name = service_name_raw.upper()
if idx_cert is not None and len(line) > idx_addr:
addr_raw = line[idx_addr:idx_cert]
else:
addr_raw = line[idx_addr:]
address = addr_raw.strip()
# empty address -> no restrictions
if not address:
alert(f"{service_name} has no IP restriction")
risks_found = True
else:
ok(f"{service_name} restricted to: {address}")
if not risks_found:
ok("All RMI services have proper IP restrictions")
# WiFi / PMKID / WPS check (/interface/wifi/print detail, RouterOS v7+)
def check_wifi_security(connection):
section("WiFi Security")
try:
output = connection.send_command("/interface/wifi/print detail")
except Exception as e:
error(f"Error while checking WiFi: {e}")
return
if not output.strip():
ok("No WiFi interfaces found")
return
interfaces = output.split("\n\n")
risks_found = False
# scan interfaces
for iface in interfaces:
if not iface.strip():
continue
name_match = re.search(r'\bname="([^"]+)"', iface)
if not name_match:
name_match = re.search(r'\bdefault-name="([^"]+)"', iface)
if name_match:
iface_name = name_match.group(1)
else:
iface_name = "Unknown"
pmkid_enabled = re.search(r'\.disable-pmkid=no\b', iface)
wps_push = re.search(r'\.wps=push-button\b', iface)
if pmkid_enabled or wps_push:
warn(f"WiFi interface '{iface_name}' has potentially weak security settings")
if pmkid_enabled:
detail("PMKID is enabled (.disable-pmkid=no) - allows offline PMKID-based attacks on WPA/WPA2-PSK")
if wps_push:
detail("WPS push-button is enabled (.wps=push-button) - WPS is a known attack surface; disable it in production")
risks_found = True
if not risks_found:
ok("No risky WiFi security settings detected")
# UPnP check
def check_upnp_status(connection):
section("UPnP Status")
output = connection.send_command("/ip upnp print")
if "enabled: yes" in output:
alert("UPnP is enabled")
detail("UPnP allows automatic port forwarding to internal hosts")
detail("Can expose devices to the Internet without your awareness")
detail("Ensure this was intentionally enabled")
else:
ok("UPnP is disabled")
# DNS behavior check
def check_dns_status(connection):
section("DNS Settings")
output = connection.send_command("/ip dns print")
if "allow-remote-requests: yes" in output:
warn("Router is acting as a DNS server")
detail("DNS queries from the network are accepted")
detail("Ensure DNS is not exposed on external interfaces")
else:
ok("Remote DNS requests are disabled")
# DDNS check
def check_ddns_status(connection):
section("DDNS Settings")
output = connection.send_command("/ip cloud print")
if "ddns-enabled: yes" in output:
warn("Dynamic DNS is enabled")
detail("Your router may become reachable via a public hostname")
detail("Ensure this is needed for remote access or VPN setups")
else:
ok("DDNS is disabled")
# PoE check
def check_poe_status(connection):
section("PoE Status")
output = connection.send_command("/interface ethernet print detail")
interfaces = output.split("\n\n")
risks_found = False
# inspect each port
for iface in interfaces:
if not iface.strip():
continue
name_match = re.search(r'name="([^"]+)"', iface)
poe_match = re.search(r'poe-out=(\S+)', iface)
name = name_match.group(1) if name_match else "Unknown"
poe_mode = poe_match.group(1) if poe_match else "none"
if poe_mode in ("auto-on", "forced-on"):
warn(f"PoE is enabled on interface '{name}'")
detail("Ensure connected devices support PoE to avoid hardware damage")
risks_found = True
if not risks_found:
ok("No PoE-enabled interfaces detected")
# RouterBOOT protection check
def check_routerboot_protection(connection):
section("RouterBOOT Protection")
output = connection.send_command("/system routerboard settings print")
if "protected-routerboot: disabled" in output:
alert("RouterBOOT protection is disabled")
detail("Device can be reset or reflashed via Netinstall without authentication")
detail("Enable 'protected-routerboot' to prevent unauthorized boot changes")
else:
ok("RouterBOOT protection is enabled")
# SOCKS proxy check
def check_socks_status(connection):
section("SOCKS Proxy Status")
output = connection.send_command("/ip socks print")
if "enabled: yes" in output:
alert("SOCKS proxy is enabled")
detail("SOCKS may indicate unauthorized tunneling or compromise")
detail("Attackers often use SOCKS as a pivot into internal networks")
detail("Disable unless explicitly required for your environment")
else:
ok("SOCKS proxy is disabled")
# bandwidth-server check
def check_bandwidth_server_status(connection):
section("Bandwidth Server Status")
output = connection.send_command("/tool bandwidth-server print")
if "enabled: yes" in output:
warn("Bandwidth server is enabled")
detail("May generate unwanted test traffic")
detail("Can increase CPU load under active use")
else:
ok("Bandwidth server is disabled")
# neighbor discovery config check
def check_neighbor_discovery(connection):
section("Neighbor Discovery Protocols")
output = connection.send_command("/ip neighbor discovery-settings print")
risks_found = False
if "discover-interface-list: all" in output:
warn("Discovery packets are sent on all interfaces")
detail("This allows attackers to map RouterOS presence on multiple segments")
risks_found = True
protocol_match = re.search(r'protocol: ([\w,]+)', output)
if protocol_match:
protocols = protocol_match.group(1)
warn(f"Neighbor discovery protocols enabled: {protocols}")
detail("Limit discovery to management or trusted interfaces only")
risks_found = True
if not risks_found:
ok("No security risks found in Neighbor Discovery configuration")
# password policy check
def check_password_length_policy(connection):
section("Password Policy")
output = connection.send_command("/user settings print")
if "minimum-password-length: 0" in output:
warn("No minimum password length is enforced")
detail("Short passwords significantly reduce brute-force resistance")
detail("Set a minimum length (e.g. 10-12 characters or more)")
else:
ok("Password length policy is enforced")
# SSH security check
def check_ssh_security(connection):
section("SSH Security")
output = connection.send_command("/ip ssh print")
risks_found = False
if "forwarding-enabled: both" in output:
warn("SSH dynamic port forwarding is enabled")
detail("May be used as a tunneling/pivoting channel")
detail("Verify this is required and properly restricted")
risks_found = True
if "strong-crypto: no" in output:
warn("Strong SSH crypto is disabled")
detail("Enable 'strong-crypto' to enforce stronger ciphers and MACs")
detail("Disables weak algorithms (MD5, null encryption, small DH groups)")
risks_found = True
if not risks_found:
ok("SSH security settings are properly configured")
# Connection tracking check
def check_connection_tracking(connection):
section("Connection Tracking")
output = connection.send_command("/ip firewall connection tracking print")
if "enabled: auto" in output or "enabled: on" in output:
warn("Connection tracking is enabled")
detail("RouterOS tracks connection states for firewall/NAT")
detail("On pure transit routers without NAT, disabling may reduce CPU load")
else:
ok("Connection tracking is configured appropriately")
# RoMON check
def check_romon_status(connection):
section("RoMON Status")
output = connection.send_command("/tool romon print")
if "enabled: yes" in output:
warn("RoMON is enabled")
detail("Provides Layer 2 management access to RouterOS devices")
detail("Disable RoMON if not explicitly required to reduce attack surface")
else:
ok("RoMON is disabled")
# MAC Winbox / MAC Telnet / MAC ping checks
def check_mac_winbox_security(connection):
section("Winbox MAC Server Settings")
# MAC Winbox
try:
output = connection.send_command("/tool mac-server mac-winbox print")
if "allowed-interface-list" in output:
if "allowed-interface-list: all" in output:
warn("MAC Winbox access is allowed on all interfaces")
detail("Limit MAC Winbox to management or trusted segments only")
else:
ok("MAC Winbox is restricted to specific interfaces")
else:
# legacy layout
if re.search(r"\bINTERFACE\s*\n.*\ball\b", output, re.DOTALL | re.IGNORECASE):
warn("MAC Winbox access is allowed on all interfaces (legacy format)")
detail("Limit MAC Winbox to management or trusted segments only")
else:
ok("MAC Winbox is properly restricted (legacy format)")
except Exception as e:
error(f"Error while checking MAC Winbox: {e}")
# MAC Telnet
try:
output = connection.send_command("/tool mac-server print")
if "allowed-interface-list" in output:
if "allowed-interface-list: all" in output:
warn("MAC Telnet access is allowed on all interfaces")
detail("Limit MAC Telnet to management or trusted segments only")
else:
ok("MAC Telnet is restricted to specific interfaces")
else:
if re.search(r"\bINTERFACE\s*\n.*\ball\b", output, re.DOTALL | re.IGNORECASE):
warn("MAC Telnet access is allowed on all interfaces (legacy format)")
detail("Limit MAC Telnet to management or trusted segments only")
else:
ok("MAC Telnet is properly restricted (legacy format)")
except Exception as e:
error(f"Error while checking MAC Telnet: {e}")
# MAC ping
try:
output = connection.send_command("/tool mac-server ping print")
if "enabled: yes" in output:
warn("MAC Ping is enabled")
detail("May generate unnecessary Layer 2 broadcast traffic")
else:
ok("MAC Ping is restricted or disabled")
except Exception as e:
error(f"Error while checking MAC Ping: {e}")
# SNMP communities check
def check_snmp(connection):
section("SNMP Community Strings")
output = connection.send_command("/snmp community print")
bad_names = {"public", "private", "admin", "mikrotik", "mikrotik_admin", "root", "routeros", "zabbix"}
risks_found = False
# scan table
for line in output.splitlines():
match = re.search(r'^\s*\d+\s+[*X]?\s*([\w-]+)', line)
if not match:
continue
community_name = match.group(1).lower()
if community_name in bad_names:
warn(f"Weak SNMP community string detected: '{community_name}'")
detail("Change it to a long, random value and restrict source IPs")
risks_found = True
if not risks_found:
ok("SNMP community strings checked - no weak values detected")
# dst-nat / netmap rules check
def check_dst_nat_rules(connection):
section("Firewall NAT Rules")
output = connection.send_command("/ip firewall nat print")
dst_nat_rules = []
for line in output.splitlines():
if "action=dst-nat" in line or "action=netmap" in line:
dst_nat_rules.append(line.strip())
if dst_nat_rules:
warn("Destination NAT (dst-nat/netmap) rules detected")
detail("Exposing internal services to the Internet can be dangerous")
detail("Verify that each rule is intentional and properly restricted")
for rule in dst_nat_rules:
detail(rule)
else:
ok("No Destination NAT (dst-nat/netmap) rules detected")
# scheduler / persistence check
def detect_malicious_schedulers(connection):
section("Schedulers & Persistence")
output = connection.send_command("/system scheduler print detail")
risks_found = False
fetch_files = set()
tasks = output.split("\n\n")
# first pass: track fetch targets
for task in tasks:
if not task.strip():
continue
event_match = re.search(r'on-event="?([^"\n]+)"?', task)
event = event_match.group(1).strip() if event_match else ""
fetch_match = re.search(r'dst-path=([\S]+)', event)
if "fetch" in event and fetch_match:
fetched_file = fetch_match.group(1).strip(";")
fetch_files.add(fetched_file)
# second pass: analyze schedulers
for task in tasks:
if not task.strip():
continue
name_match = re.search(r'name="?([^"]+)"?', task)
event_match = re.search(r'on-event="?([^"\n]+)"?', task)
policy_match = re.search(r'policy=([\w,]+)', task)
interval_match = re.search(r'interval=(\d+)([smhd])', task)
name = name_match.group(1) if name_match else "Unknown"
event = event_match.group(1).strip() if event_match else ""
policies = policy_match.group(1).split(",") if policy_match else []
interval_value, interval_unit = (int(interval_match.group(1)), interval_match.group(2)) if interval_match else (None, None)
issues = []
# fetch + import chain
import_match = re.search(r'import\s+([\S]+)', event)
if "import" in event and import_match:
imported_file = import_match.group(1).strip(";")
if imported_file in fetch_files:
issues.append("Imports a previously fetched script - potential backdoor")
if interval_value and interval_unit:
issues.append(f"Runs every {interval_value}{interval_unit}, ensures persistence")
# dangerous policies
dangerous_policies = {"password", "sensitive", "sniff", "ftp"}
used_dangerous = [p for p in policies if p in dangerous_policies]
if used_dangerous:
issues.append(f"Uses high-privilege policies: {', '.join(used_dangerous)}")
# reboots
if "reboot" in event:
if interval_value and interval_unit in ["s", "m", "h"] and interval_value < 12:
issues.append(f"Frequently reboots router ({interval_value}{interval_unit}) - possible anti-forensics")
else:
issues.append("Schedules router reboot - verify it is intentional")
# tight intervals
if interval_value and interval_unit in ["s", "m", "h"] and interval_value < 25:
issues.append(f"Executes too frequently ({interval_value}{interval_unit}) - may indicate persistence or botnet-like activity")
if issues:
alert(f"Scheduler '{name}' looks suspicious")
for msg in issues:
detail(msg)
risks_found = True
if not risks_found:
ok("No suspicious schedulers detected")
# static DNS entries check
def check_static_dns_entries(connection):
section("Static DNS Entries")
output = connection.send_command("/ip dns static print detail")
dns_entries = []
entry_blocks = output.split("\n\n")
# parse entries
for entry in entry_blocks:
if not entry.strip():
continue
name_match = re.search(r'name="([^"]+)"', entry)
address_match = re.search(r'address=([\d.]+)', entry)
if name_match and address_match:
name = name_match.group(1)
address = address_match.group(1)
dns_entries.append((name, address))
if dns_entries:
warn("Static DNS entries are configured")
detail("Verify that each record is legitimate and expected")
for name, address in dns_entries:
detail(f"{name}{address}")
detail("Attackers often modify DNS for phishing or traffic redirection during post-exploitation")
else:
ok("No static DNS entries found")
# parse audit profiles
def parse_profiles(profiles_str):
raw = [p.strip().lower() for p in profiles_str.split(",")]
selected = {p for p in raw if p}
valid = {"system", "protocols", "wifi"}
if not selected:
error("No profiles specified")
info("Use at least one profile: system, protocols, wifi")
sys.exit(1)
invalid = selected - valid
if invalid:
error("Unknown profiles: " + ", ".join(sorted(invalid)))
info("Valid profiles: system, protocols, wifi")
sys.exit(1)
return selected
# main audit dispatcher
def run_sara_audit(args):
section("Sara Audit Mode")
info(f"Target Device: {args.ip}")
info(f"Transport: SSH (port {args.port})")
profiles = parse_profiles(args.profiles)
password, key_file, key_passphrase = normalize_auth_and_prompt(args)
connection = connect_to_router(
args.ip,
args.username,
password=password,
port=args.port,
key_file=key_file,
key_passphrase=key_passphrase,
)
# system profile
if "system" in profiles:
check_routeros_version(connection)
check_default_users(connection)
check_rmi_services(connection)
checking_access_to_RMI(connection)
check_poe_status(connection)
check_routerboot_protection(connection)
check_bandwidth_server_status(connection)
check_password_length_policy(connection)
check_ssh_security(connection)
check_connection_tracking(connection)
check_romon_status(connection)
check_mac_winbox_security(connection)
check_dst_nat_rules(connection)
detect_malicious_schedulers(connection)
# protocols profile
if "protocols" in profiles:
check_smb(connection)
check_upnp_status(connection)
check_socks_status(connection)
check_dns_status(connection)
check_static_dns_entries(connection)
check_ddns_status(connection)
check_neighbor_discovery(connection)
check_snmp(connection)
# wifi profile
if "wifi" in profiles:
check_wifi_security(connection)
connection.disconnect()
print(f"[*] Disconnected from RouterOS ({args.ip})")
# CVE command dispatcher
def run_cve_command(args):
# mode 1: manual version
if args.mode_or_ip.lower() == "version":
version = args.username_or_version.strip()
section("CVE Search (Manual)")
info(f"RouterOS Version: {version}")
# always pass string here
run_cve_audit_for_version(version)
return
# mode 2: live device
ip = args.mode_or_ip
username = args.username_or_version
# reuse auth helper
args.ip = ip
args.username = username
section("CVE Search (Live)")
info(f"Target Device: {ip}")
info(f"Transport: SSH (port {args.port})")
password, key_file, key_passphrase = normalize_auth_and_prompt(args)
connection = connect_to_router(
ip,
username,
password=password,
port=args.port,
key_file=key_file,
key_passphrase=key_passphrase,
)
# here we pass connection, not version string
run_cve_audit(connection)
connection.disconnect()
print(f"[*] Disconnected from RouterOS ({ip})")
def main():
banner()
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
# Audit mode
audit = sub.add_parser("audit", help="Run RouterOS security configuration audit")
audit.add_argument("ip", help="RouterOS IP address")
audit.add_argument("username", help="SSH username")
audit.add_argument("profiles", help="Profiles: system,protocols,wifi (comma-separated)")
audit.add_argument("key", nargs="?", default=None, help="Path to SSH private key (optional)")
audit.add_argument("port", nargs="?", type=int, default=22, help="SSH port (default: 22)")
audit.set_defaults(func=run_sara_audit)
# CVE mode
cve = sub.add_parser("cve", help="Run RouterOS CVE audit (live or by version)")
cve.add_argument("mode_or_ip", help="'version' or RouterOS IP address")
cve.add_argument("username_or_version", help="SSH username or RouterOS version string")
cve.add_argument("key", nargs="?", default=None, help="Path to SSH private key (optional)")
cve.add_argument("port", nargs="?", type=int, default=22, help="SSH port (default: 22)")
cve.set_defaults(func=run_cve_command)
# no args = help
if len(sys.argv) == 1:
parser.print_help()
sys.exit(0)
try:
# dispatch
args = parser.parse_args()
args.func(args)
except KeyboardInterrupt:
print()
error("Interrupted by user")
sys.exit(1)
if __name__ == "__main__":
main()