Mini Shell
from typing import Union, Literal
from pathlib import Path
import re
import subprocess
import netaddr
from output import err_exit, print_listed, warn
from run_cmd import is_exe, which
from rads.color import yellow
def fw_info() -> tuple[
Literal['APF', 'CSF', 'ipset+fail2ban'],
Literal['/usr/local/sbin/apf', '/usr/sbin/csf', None],
Union[list[dict[str, str]], str],
]:
"""Yields a tuple of fw_name, fw_command, fw_data.
fw_name will be "APF", "CSF", or "ipset+fail2ban".
If fw_name was "APF" or "CSF", fw_command will be the path to its exe.
If fw_name was "APF" or "CSF", fw_data will be the contents of its deny
file. Otherwise, fw_data will be a list of dicts containing "listname"
and "ip".
Returns:
tuple[str, str | None, list[dict[str, str] | None]]: see above
"""
if is_exe('/usr/local/sbin/apf'):
fw_cmd = '/usr/local/sbin/apf'
deny_file = Path('/etc/apf/deny_hosts.rules')
name = 'APF'
elif is_exe('/usr/sbin/csf'):
fw_cmd = '/usr/sbin/csf'
deny_file = Path('/etc/csf/csf.deny')
name = 'CSF'
elif is_exe('/opt/imh-python/bin/fail2ban-client') and which('ipset'):
name = 'ipset+fail2ban'
deny_file = None
fw_cmd = None
else:
err_exit('Cannot identify firewall')
if deny_file is None:
deny_data = list(read_ipset_save())
else:
try:
deny_data = deny_file.read_text(encoding='utf-8')
except FileNotFoundError:
err_exit(f'Cannot read {deny_file}. Firewall is misconfigured.')
return name, fw_cmd, deny_data
def read_ipset_save():
irgx = re.compile(r'add (?P<listname>[a-zA-Z0-9\-_]+) (?P<ip>[0-9\./]+)$')
with subprocess.Popen(
['ipset', 'save'],
encoding='utf-8',
stdout=subprocess.PIPE,
universal_newlines=True,
) as proc:
for line in proc.stdout:
if match := irgx.match(line.rstrip()):
yield match.groupdict()
def ipset_list_action(
listname: str,
) -> Literal['ACCEPT', 'DROP', 'DENY', 'UNKNOWN']:
"""Check whether an ipset list is set to ACCEPT, DROP, or DENY"""
try:
iptables = subprocess.check_output(
['iptables', '-nL'], encoding='utf-8'
)
except (OSError, subprocess.CalledProcessError):
err_exit('Failed to execute iptables to determine list type')
ipt_data = [x for x in iptables.splitlines() if x.find('match-set') > 0]
for tline in ipt_data:
if listname == tline.split()[6]:
return tline.split()[0]
return 'UNKNOWN'
def ipset_fail2ban_check(
fw_data: list[dict[str, str]], ipaddr: netaddr.IPAddress
) -> tuple[bool, Union[str, None]]:
"""Check deny_data ``fw_info()`` for an IP address. If found, return whether
it's blocked and in what fail2ban list if it was automatically blocked
Args:
fw_data (list[dict[str, str]]): third arg returned by ``fw_info()``
ipaddr (netaddr.IPAddress): IP address to check
Returns:
tuple[bool, str | None]]: if blocked and in what fail2ban list if any
"""
list_name = None
for tnet in fw_data:
try:
listed = ipaddr in netaddr.IPNetwork(tnet['ip'])
if listed:
list_name = tnet['listname']
break
except netaddr.AddrFormatError:
continue
list_action = ipset_list_action(list_name)
if not listed:
print_listed(ipaddr, False, 'any ipset or fail2ban list')
return False, None
print_listed(ipaddr, True, f'the {list_name} {list_action} list')
if list_action == 'ACCEPT':
warn(f'{ipaddr} is NOT BLOCKED. It is whitelisted.', color=yellow)
return False, None
if list_name.startswith('f2b-'):
warn(
'Automatically blocked by fail2ban in jail:',
list_name.replace('f2b-', ''),
color=yellow,
)
return listed, list_name.replace('f2b-', '')
return listed, None
def check_iptables(ipaddr: netaddr.IPAddress) -> bool:
"""Search iptables -nL for a line containing an IP which does not start with
ACCEPT"""
try:
fw_data = subprocess.check_output(['iptables', '-nL'], encoding='utf-8')
except (OSError, subprocess.CalledProcessError):
# stderr will print to tty
err_exit('could not run iptables -nL')
for line in fw_data.splitlines():
if not line.startswith('ACCEPT') and str(ipaddr) in line:
return True
return False
Zerion Mini Shell 1.0