Replace so-firewall

This commit is contained in:
Mike Reeves
2022-09-20 11:11:19 -04:00
parent 512c044d80
commit 9fffe1b5fa
2 changed files with 87 additions and 519 deletions

View File

@@ -1,142 +1,11 @@
#!/usr/bin/env python3
#!/usr/bin/env bash
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
# https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0.
import ipaddress
import textwrap
import os
import subprocess
import sys
import argparse
import re
from lxml import etree as ET
from datetime import datetime as dt
from datetime import timezone as tz
LOCAL_SALT_DIR='/opt/so/saltstack/local'
VALID_ROLES = {
'a': { 'role': 'analyst','desc': 'Analyst - 80/tcp, 443/tcp' },
'b': { 'role': 'beats_endpoint', 'desc': 'Logstash Beat - 5044/tcp' },
'e': { 'role': 'elasticsearch_rest', 'desc': 'Elasticsearch REST API - 9200/tcp' },
'f': { 'role': 'strelka_frontend', 'desc': 'Strelka frontend - 57314/tcp' },
's': { 'role': 'syslog', 'desc': 'Syslog device - 514/tcp/udp' },
't': { 'role': 'elastic_agent_endpoint', 'desc': 'Elastic Agent endpoint - 8220/tcp,5055/tcp' }
}
def validate_ip_cidr(ip_cidr: str) -> bool:
try:
ipaddress.ip_address(ip_cidr)
except ValueError:
try:
ipaddress.ip_network(ip_cidr)
except ValueError:
return False
return True
def role_prompt() -> str:
print()
print('Choose the role for the IP or Range you would like to allow')
print()
for role in VALID_ROLES:
print(f'[{role}] - {VALID_ROLES[role]["desc"]}')
print()
role = input('Please enter your selection: ')
if role in VALID_ROLES.keys():
return VALID_ROLES[role]['role']
else:
print(f'Invalid role \'{role}\', please try again.', file=sys.stderr)
sys.exit(1)
def ip_prompt() -> str:
ip = input('Enter a single ip address or range to allow (ex: 10.10.10.10 or 10.10.0.0/16): ')
if validate_ip_cidr(ip):
return ip
else:
print(f'Invalid IP address or CIDR block \'{ip}\', please try again.', file=sys.stderr)
sys.exit(1)
def apply(role: str, ip: str) -> int:
firewall_cmd = ['so-firewall', 'includehost', role, ip]
salt_cmd = ['salt-call', 'state.apply', '-l', 'quiet', 'firewall', 'queue=True']
print(f'Adding {ip} to the {role} role. This can take a few seconds...')
cmd = subprocess.run(firewall_cmd)
if cmd.returncode == 0:
cmd = subprocess.run(salt_cmd, stdout=subprocess.DEVNULL)
else:
return cmd.returncode
def main():
if os.geteuid() != 0:
print('You must run this script as root', file=sys.stderr)
sys.exit(1)
main_parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent(f'''\
additional information:
To use this script in interactive mode call it with no arguments
'''
))
group = main_parser.add_argument_group(title='roles')
group.add_argument('-a', dest='roles', action='append_const', const=VALID_ROLES['a']['role'], help="Analyst - 80/tcp, 443/tcp")
group.add_argument('-b', dest='roles', action='append_const', const=VALID_ROLES['b']['role'], help="Logstash Beat - 5044/tcp")
group.add_argument('-e', dest='roles', action='append_const', const=VALID_ROLES['e']['role'], help="Elasticsearch REST API - 9200/tcp")
group.add_argument('-f', dest='roles', action='append_const', const=VALID_ROLES['f']['role'], help="Strelka frontend - 57314/tcp")
group.add_argument('-s', dest='roles', action='append_const', const=VALID_ROLES['s']['role'], help="Syslog device - 514/tcp/udp")
group.add_argument('-t', dest='roles', action='append_const', const=VALID_ROLES['t']['role'], help="Elastic Agent endpoint - 8220/tcp,5055/tcp")
ip_g = main_parser.add_argument_group(title='allow')
ip_g.add_argument('-i', help="IP or CIDR block to disallow connections from, requires at least one role argument", metavar='', dest='ip')
args = main_parser.parse_args(sys.argv[1:])
if args.roles is None:
role = role_prompt()
ip = ip_prompt()
try:
return_code = apply(role, ip)
except Exception as e:
print(f'Unexpected exception occurred: {e}', file=sys.stderr)
return_code = e.errno
sys.exit(return_code)
elif args.roles is not None and args.ip is None:
if os.environ.get('IP') is None:
main_parser.print_help()
sys.exit(1)
else:
args.ip = os.environ['IP']
if validate_ip_cidr(args.ip):
try:
for role in args.roles:
return_code = apply(role, args.ip)
if return_code > 0:
break
except Exception as e:
print(f'Unexpected exception occurred: {e}', file=sys.stderr)
return_code = e.errno
else:
print(f'Invalid IP address or CIDR block \'{args.ip}\', please try again.', file=sys.stderr)
return_code = 1
sys.exit(return_code)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
sys.exit(1)
echo "Please use the Configuration section in SOC to allow hosts"
echo ""
echo "If you need command line options on adding hosts please run so-firewall"

View File

@@ -1,401 +1,100 @@
#!/usr/bin/env python3
#!/usr/bin/env bash
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
# https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0.
. /usr/sbin/so-common
import os
import re
import subprocess
import sys
import time
import yaml
if [[ $# -lt 1 ]]; then
echo "Usage: $0 --role=<ROLE> --ip=<IP ADDRESS> --apply=<true|false>"
echo ""
echo " Example: so-firewall --role=sensor --ip=192.168.254.100 --apply=true"
echo ""
exit 1
fi
lockFile = "/tmp/so-firewall.lock"
hostgroupsFilename = "/opt/so/saltstack/local/salt/firewall/hostgroups.local.yaml"
portgroupsFilename = "/opt/so/saltstack/local/salt/firewall/portgroups.local.yaml"
defaultPortgroupsFilename = "/opt/so/saltstack/default/salt/firewall/portgroups.yaml"
supportedProtocols = ['tcp', 'udp']
readonly = False
for i in "$@"; do
case $i in
-r=*|--role=*)
ROLE="${i#*=}"
shift
;;
-i=*|--ip=*)
IP="${i#*=}"
shift
;;
-a=*|--apply*)
APPLY="${i#*=}"
shift
;;
-*|--*)
echo "Unknown option $i"
exit 1
;;
*)
;;
esac
done
def showUsage(options, args):
print('Usage: {} [OPTIONS] <COMMAND> [ARGS...]'.format(sys.argv[0]))
print(' Options:')
print(' --apply - After updating the firewall configuration files, apply the new firewall state')
print(' --defaultports - Read port groups from default configuration files instead of local configuration.')
print('')
print(' General commands:')
print(' help - Prints this usage information.')
print(' apply - Apply the firewall state.')
print('')
print(' Host commands:')
print(' listhostgroups - Lists the known host groups.')
print(' includedhosts - Lists the IPs included in the given group. Args: <GROUP_NAME>')
print(' excludedhosts - Lists the IPs excluded from the given group. Args: <GROUP_NAME>')
print(' includehost - Includes the given IP in the given group. Args: <GROUP_NAME> <IP>')
print(' excludehost - Excludes the given IP from the given group. Args: <GROUP_NAME> <IP>')
print(' removehost - Removes an excluded IP from the given group. Args: <GROUP_NAME> <IP>')
print(' addhostgroup - Adds a new, custom host group. Args: <GROUP_NAME>')
print('')
print(' Port commands:')
print(' listportgroups - Lists the known port groups.')
print(' listports - Lists ports in the given group and protocol. Args: <GROUP_NAME> <PORT_PROTOCOL>')
print(' addport - Adds a PORT to the given group. Args: <GROUP_NAME> <PORT_PROTOCOL> <PORT>')
print(' removeport - Removes a PORT from the given group. Args: <GROUP_NAME> <PORT_PROTOCOL> <PORT>')
print(' addportgroup - Adds a new, custom port group. Args: <GROUP_NAME>')
print('')
print(' Where:')
print(' GROUP_NAME - The name of an alias group (Ex: analyst)')
print(' IP - Either a single IP address (Ex: 8.8.8.8) or a CIDR block (Ex: 10.23.0.0/16).')
print(' PORT_PROTOCOL - Must be one of the following: ' + str(supportedProtocols))
print(' PORT - Either a single numeric port (Ex: 443), or a port range (Ex: 8000:8002).')
sys.exit(1)
ROLE=${ROLE,,}
APPLY=${APPLY,,}
def checkDefaultPortsOption(options):
global portgroupsFilename
if "--defaultports" in options:
portgroupsFilename = defaultPortgroupsFilename
function rolecall() {
THEROLE=$1
THEROLES="analyst analyst_workstation heavynode idhnode receiver searchnode sensor"
def checkApplyOption(options):
if "--apply" in options:
return apply(None, None)
for AROLE in $THEROLES; do
if [ "$AROLE" = "$THEROLE" ]; then
return 0
fi
done
return 1
}
def loadYaml(filename):
global readonly
# Make sure the required options are specified
if [ -z "$ROLE" ]; then
echo "Please specify a role with --role="
exit 1
fi
if [ -z "$IP" ]; then
echo "Please specify an IP address with --ip="
exit 1
fi
file = open(filename, "r")
content = file.read()
# Are we dealing with a role that this script supports?
if rolecall "$ROLE"; then
echo "$ROLE is a supported role"
else
echo "This is not a supported role"
exit 1
fi
# Remove Jinja templating (for read-only operations)
if "{%" in content or "{{" in content:
content = content.replace("{{ ssh_port }}", "22")
pattern = r'.*({%|{{|}}|%}).*'
content = re.sub(pattern, "", content)
readonly = True
local_salt_dir=/opt/so/saltstack/local/salt/firewall
return yaml.safe_load(content)
# Let's see if the file exists and if it does, let's see if the IP exists.
if [ -f "$local_salt_dir/hostgroups/$ROLE" ]; then
if grep -q $IP "$local_salt_dir/hostgroups/$ROLE"; then
echo "Host already exists"
exit 0
fi
fi
def writeYaml(filename, content):
global readonly
# If you have reached this part of your quest then let's add the IP
if [ -f "$local_salt_dir/hostgroups/$ROLE" ]; then
touch $local_salt_dir/hostgroups/$ROLE
echo "Adding $IP to the $ROLE role"
echo "$IP" > $local_salt_dir/hostgroups/$ROLE
else
echo "Adding $IP to the $ROLE role"
echo "$IP" >> $local_salt_dir/hostgroups/$ROLE
fi
if readonly:
raise Exception("Cannot write yaml file that has been flagged as read-only")
file = open(filename, "w")
return yaml.dump(content, file)
def listHostGroups():
content = loadYaml(hostgroupsFilename)
hostgroups = content['firewall']['hostgroups']
if hostgroups is not None:
for group in hostgroups:
print(group)
return 0
def listIps(name, mode):
content = loadYaml(hostgroupsFilename)
if name not in content['firewall']['hostgroups']:
print('Host group does not exist', file=sys.stderr)
return 4
hostgroup = content['firewall']['hostgroups'][name]
ips = hostgroup['ips'][mode]
if ips is not None:
for ip in ips:
print(ip)
return 0
def addIp(name, ip, mode):
content = loadYaml(hostgroupsFilename)
if name not in content['firewall']['hostgroups']:
print('Host group does not exist', file=sys.stderr)
return 4
hostgroup = content['firewall']['hostgroups'][name]
ips = hostgroup['ips'][mode]
if ips is None:
ips = []
hostgroup['ips'][mode] = ips
if ip not in ips:
ips.append(ip)
else:
print('Already exists', file=sys.stderr)
return 3
writeYaml(hostgroupsFilename, content)
return 0
def removeIp(name, ip, mode, silence = False):
content = loadYaml(hostgroupsFilename)
if name not in content['firewall']['hostgroups']:
print('Host group does not exist', file=sys.stderr)
return 4
hostgroup = content['firewall']['hostgroups'][name]
ips = hostgroup['ips'][mode]
if ips is None:
ips = []
hostgroup['ips'][mode] = ips
if ip in ips:
ips.remove(ip)
else:
if not silence:
print('IP does not exist', file=sys.stderr)
return 3
writeYaml(hostgroupsFilename, content)
return 0
def createProtocolMap():
map = {}
for protocol in supportedProtocols:
map[protocol] = []
return map
def listPortGroups():
content = loadYaml(portgroupsFilename)
portgroups = content['firewall']['aliases']['ports']
if portgroups is not None:
for group in portgroups:
print(group)
return 0
def addhostgroup(options, args):
if len(args) != 1:
print('Missing host group name argument', file=sys.stderr)
showUsage(options, args)
name = args[0]
content = loadYaml(hostgroupsFilename)
if name in content['firewall']['hostgroups']:
print('Already exists', file=sys.stderr)
return 3
content['firewall']['hostgroups'][name] = { 'ips': { 'insert': [], 'delete': [] }}
writeYaml(hostgroupsFilename, content)
return 0
def listportgroups(options, args):
if len(args) != 0:
print('Unexpected arguments', file=sys.stderr)
showUsage(options, args)
checkDefaultPortsOption(options)
return listPortGroups()
def addportgroup(options, args):
if len(args) != 1:
print('Missing port group name argument', file=sys.stderr)
showUsage(options, args)
name = args[0]
content = loadYaml(portgroupsFilename)
ports = content['firewall']['aliases']['ports']
if ports is None:
ports = {}
content['firewall']['aliases']['ports'] = ports
if name in ports:
print('Already exists', file=sys.stderr)
return 3
ports[name] = createProtocolMap()
writeYaml(portgroupsFilename, content)
return 0
def listports(options, args):
if len(args) != 2:
print('Missing port group name or port protocol', file=sys.stderr)
showUsage(options, args)
checkDefaultPortsOption(options)
name = args[0]
protocol = args[1]
if protocol not in supportedProtocols:
print('Port protocol is not supported', file=sys.stderr)
return 5
content = loadYaml(portgroupsFilename)
ports = content['firewall']['aliases']['ports']
if ports is None:
ports = {}
content['firewall']['aliases']['ports'] = ports
if name not in ports:
print('Port group does not exist', file=sys.stderr)
return 3
if protocol not in ports[name]:
print('Port group does not contain protocol', file=sys.stderr)
return 3
ports = ports[name][protocol]
if ports is not None:
for port in ports:
print(port)
return 0
def addport(options, args):
if len(args) != 3:
print('Missing port group name or port protocol, or port argument', file=sys.stderr)
showUsage(options, args)
name = args[0]
protocol = args[1]
port = args[2]
if protocol not in supportedProtocols:
print('Port protocol is not supported', file=sys.stderr)
return 5
content = loadYaml(portgroupsFilename)
ports = content['firewall']['aliases']['ports']
if ports is None:
ports = {}
content['firewall']['aliases']['ports'] = ports
if name not in ports:
print('Port group does not exist', file=sys.stderr)
return 3
ports = ports[name][protocol]
if ports is None:
ports = []
content['firewall']['aliases']['ports'][name][protocol] = ports
if port in ports:
print('Already exists', file=sys.stderr)
return 3
ports.append(port)
writeYaml(portgroupsFilename, content)
code = checkApplyOption(options)
return code
def removeport(options, args):
if len(args) != 3:
print('Missing port group name or port protocol, or port argument', file=sys.stderr)
showUsage(options, args)
name = args[0]
protocol = args[1]
port = args[2]
if protocol not in supportedProtocols:
print('Port protocol is not supported', file=sys.stderr)
return 5
content = loadYaml(portgroupsFilename)
ports = content['firewall']['aliases']['ports']
if ports is None:
ports = {}
content['firewall']['aliases']['ports'] = ports
if name not in ports:
print('Port group does not exist', file=sys.stderr)
return 3
ports = ports[name][protocol]
if ports is None or port not in ports:
print('Port does not exist', file=sys.stderr)
return 3
ports.remove(port)
writeYaml(portgroupsFilename, content)
code = checkApplyOption(options)
return code
def listhostgroups(options, args):
if len(args) != 0:
print('Unexpected arguments', file=sys.stderr)
showUsage(options, args)
return listHostGroups()
def includedhosts(options, args):
if len(args) != 1:
print('Missing host group name argument', file=sys.stderr)
showUsage(options, args)
return listIps(args[0], 'insert')
def excludedhosts(options, args):
if len(args) != 1:
print('Missing host group name argument', file=sys.stderr)
showUsage(options, args)
return listIps(args[0], 'delete')
def includehost(options, args):
if len(args) != 2:
print('Missing host group name or ip argument', file=sys.stderr)
showUsage(options, args)
result = addIp(args[0], args[1], 'insert')
if result == 0:
removeIp(args[0], args[1], 'delete', True)
code = result
if code == 0:
code = checkApplyOption(options)
return code
def excludehost(options, args):
if len(args) != 2:
print('Missing host group name or ip argument', file=sys.stderr)
showUsage(options, args)
result = addIp(args[0], args[1], 'delete')
if result == 0:
removeIp(args[0], args[1], 'insert', True)
code = result
if code == 0:
code = checkApplyOption(options)
return code
def removehost(options, args):
if len(args) != 2:
print('Missing host group name or ip argument', file=sys.stderr)
showUsage(options, args)
code = removeIp(args[0], args[1], 'delete')
if code == 0:
code = checkApplyOption(options)
return code
def apply(options, args):
proc = subprocess.run(['salt-call', 'state.apply', 'firewall', 'queue=True'])
return proc.returncode
def main():
options = []
args = sys.argv[1:]
for option in args:
if option.startswith("--"):
options.append(option)
args.remove(option)
if len(args) == 0:
showUsage(options, None)
commands = {
"help": showUsage,
"listhostgroups": listhostgroups,
"includedhosts": includedhosts,
"excludedhosts": excludedhosts,
"includehost": includehost,
"excludehost": excludehost,
"removehost": removehost,
"listportgroups": listportgroups,
"listports": listports,
"addport": addport,
"removeport": removeport,
"addhostgroup": addhostgroup,
"addportgroup": addportgroup,
"apply": apply
}
code=1
try:
lockAttempts = 0
maxAttempts = 30
while lockAttempts < maxAttempts:
lockAttempts = lockAttempts + 1
try:
f = open(lockFile, "x")
f.close()
break
except:
time.sleep(2)
if lockAttempts == maxAttempts:
print("Lock file (" + lockFile + ") could not be created; proceeding without lock.")
cmd = commands.get(args[0], showUsage)
code = cmd(options, args[1:])
finally:
try:
os.remove(lockFile)
except:
print("Lock file (" + lockFile + ") already removed")
sys.exit(code)
if __name__ == "__main__":
main()
# Check to see if we are applying this right away.
if [ "$APPLY" = "true" ]; then
echo "Applying the firewall rules"
salt-call state.apply firewall queue=True
else
echo "Firewall rules will be applied next salt run"
fi