Compare commits
	
		
			21 Commits
		
	
	
		
			542718b956
			...
			develop
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6f36df2195 | |||
| c991df53fa | |||
| f39d319b25 | |||
| 3e64189f8f | |||
| 45dc173ea7 | |||
| 9b85bfabdb | |||
| 969ba0f64c | |||
| d9b5d36835 | |||
| 9f86e143fe | |||
| a49da1f3ef | |||
| 36ff86c71e | |||
| 29f6e6093b | |||
| ccc7165d1b | |||
| 72f0e095ca | |||
| 61869049a0 | |||
| c868c63aa7 | |||
| 9875dccec0 | |||
| 359514e581 | |||
| 9ed6b65b6d | |||
| 1c2e3413ec | |||
| 4ca4233892 | 
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,5 +1,7 @@ | |||||||
| venv/ | venv/ | ||||||
|  |  | ||||||
|  | *.sw? | ||||||
|  |  | ||||||
| *.pyc | *.pyc | ||||||
| __pycache__/ | __pycache__/ | ||||||
|  |  | ||||||
| @ -12,3 +14,6 @@ htmlcov/ | |||||||
| dist/ | dist/ | ||||||
| build/ | build/ | ||||||
| *.egg-info/ | *.egg-info/ | ||||||
|  |  | ||||||
|  | coverage.xml | ||||||
|  | report.xml | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| run tests: | run tests: | ||||||
|   stage: test |   stage: test | ||||||
|   image: python:3.8 |   image: python:3.9 | ||||||
|   script: |   script: | ||||||
|     - pip install pytest pytest-cov pytest-mock pytest-flask |     - pip install pytest pytest-cov pytest-mock pytest-flask | ||||||
|     - pip install Flask-HTTPAuth |     - pip install Flask-HTTPAuth | ||||||
| @ -11,7 +11,9 @@ run tests: | |||||||
|   artifacts: |   artifacts: | ||||||
|     when: always |     when: always | ||||||
|     reports: |     reports: | ||||||
|       cobertura: coverage.xml |       coverage_report: | ||||||
|  |         coverage_format: cobertura | ||||||
|  |         path: coverage.xml | ||||||
|       junit: report.xml |       junit: report.xml | ||||||
|   tags: |   tags: | ||||||
|     - docker |     - docker | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								CHANGELOG
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								CHANGELOG
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | - 2023.1 | ||||||
|  |  | ||||||
|  | * Implement #3, a /ping health check endpoint | ||||||
							
								
								
									
										130
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,130 @@ | |||||||
|  | [](https://gitlab.niet.verweg.com/ruben/jail2ban-pf/-/commits/main) | ||||||
|  | [](https://gitlab.niet.verweg.com/ruben/jail2ban-pf/-/commits/main) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Installation | ||||||
|  |  | ||||||
|  |  | ||||||
|  | * Install uwsgi  | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | sudo pkg install www/uwsgi | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | * Clone this repository | ||||||
|  |  | ||||||
|  | ## Configuration | ||||||
|  |  | ||||||
|  | ### rc.conf | ||||||
|  |  | ||||||
|  | * Use the following for configuring uwsgi in rc.conf | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | sudo sysrc uwsgi\_enable="YES" | ||||||
|  | sudo sysrc uwsgi\_profiles="jail2ban\_pf" | ||||||
|  | sudo sysrc uwsgi\_jail2ban\_pf\_flags="-L -M --uid \_jail2ban --python-path /opt/jail2ban-pf --wsgi-file /opt/jail2ban-pf/wsgi.py --stats 127.0.0.1:9191 --socket 127.0.0.1:3031 --chdir /var/empty --callable app --manage-script-name" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### jail2ban | ||||||
|  |  | ||||||
|  | * Configure <installation root>/instance/config.py | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | SECRET\_KEY = os.urandom(32).hex() | ||||||
|  | AUTHFILE = '/usr/local/etc/jail2ban-pf-users.txt' | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### nginx | ||||||
|  |  | ||||||
|  | * Configure a nginx upstream and vhost | ||||||
|  |  | ||||||
|  | _Of course you can listen on ipv4/ipv6 but you want to protect these addresses from inadvertent or malicious probes_ | ||||||
|  |  | ||||||
|  |     upstream uwsgi_pf_jail2ban { | ||||||
|  |         server 127.0.0.1:3031; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     server { | ||||||
|  |         listen       unix:/path/to/jail_1/var/run/pf2ban/pf_jail2ban.sock; | ||||||
|  |         listen       unix:/path/to/jail_2/var/run/pf2ban/pf_jail2ban.sock; | ||||||
|  |         listen       unix:/path/to/jail_3/var/run/pf2ban/pf_jail2ban.sock; | ||||||
|  |         server_name  _; | ||||||
|  |  | ||||||
|  |         location / { | ||||||
|  |             index     index.html index.htm index.php; | ||||||
|  |             allow all; | ||||||
|  |             include /usr/local/etc/nginx/uwsgi_params-dist; | ||||||
|  |             uwsgi_pass uwsgi_pf_jail2ban; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | ### /etc/pf.conf | ||||||
|  |  | ||||||
|  | * Place anchors in pf for jail2ban to use. You probably want to place the early in your existing pf configuration | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | anchor "f2b/*" | ||||||
|  | anchor f2b-jail { | ||||||
|  |     anchor "jail1_fqdn" to { <addr_jail1>, <addr_extra_jail1>, <addr_extra6_jail1> } | ||||||
|  |     anchor "jail2_fqdn" to { <addr_jail2>, <addr_extra_jail2>, <addr_extra6_jail2> } | ||||||
|  |     anchor "jail3_fqdn" to { <addr_jail3>, <addr_extra_jail3>, <addr_extra6_jail3> } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Having seperate anchors per jail makes it possible to have fine grained | ||||||
|  | blocking: Something that is harmful to jail2 might be perfectly legit for jail2. | ||||||
|  |  | ||||||
|  | #### Checking rules/tables made with fail2ban/jail2ban | ||||||
|  | Fail2ban will (re)create the per anchor rules on startup, and populate the designated address tables with offenders, e.g.: | ||||||
|  |  | ||||||
|  |     sudo pfctl -a f2b-jail/jail1\_fqdn -T show -t f2b-recidive | ||||||
|  |     192.0.2.66 | ||||||
|  |     2001:db8:abad:cafe:0bad:f00d | ||||||
|  |  | ||||||
|  | And the rules referencing these tables | ||||||
|  |  | ||||||
|  |     sudo pfctl -a 'f2b-jail/jail1\_fqdn' -s rules | ||||||
|  |     block drop quick proto tcp from <f2b-dovecot> to any port = pop3 | ||||||
|  |     block drop quick proto tcp from <f2b-dovecot> to any port = pop3s | ||||||
|  |     block drop quick proto tcp from <f2b-dovecot> to any port = imap | ||||||
|  |     block drop quick proto tcp from <f2b-dovecot> to any port = imaps | ||||||
|  |     block drop quick proto tcp from <f2b-dovecot> to any port = submission | ||||||
|  |     block drop quick proto tcp from <f2b-dovecot> to any port = smtps | ||||||
|  |     block drop quick proto tcp from <f2b-dovecot> to any port = sieve | ||||||
|  |     block drop quick proto tcp from <f2b-sendmail-auth> to any port = submission | ||||||
|  |     block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtps | ||||||
|  |     block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtp | ||||||
|  |     block drop quick proto tcp from <f2b-sshd> to any port = ssh | ||||||
|  |     block drop quick proto tcp from <f2b-recidive> to any | ||||||
|  |      | ||||||
|  | ### fail2ban | ||||||
|  |  | ||||||
|  | * Create the following action plugin for fail2ban on the jail desiring to use fail2ban/jail2ban | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | cat <<'EOT' | tee /usr/local/etc/fail2ban/action.d/jail2ban-pf.conf > /dev/null | ||||||
|  | Definition] | ||||||
|  | actionstart = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -XPUT -H 'Content-Type: application/json' -d '{"port":"<actiontype>","name":"<name>","protocol":"<protocol>"}' http://localhost/register | ||||||
|  | actionstart_on_demand = false | ||||||
|  | actionstop = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -XDELETE -H 'Content-Type: application/json' -d '{"port":"<actiontype>","name":"<name>","protocol":"<protocol>"}' http://localhost/register | ||||||
|  | actionflush = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -X GET http://localhost/flush/<name> | ||||||
|  | actioncheck =  | ||||||
|  | actionban = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -X PUT -H 'Content-Type: application/json' -d '{"name":"<name>","ip":"<ip>"}' http://localhost/ban | ||||||
|  | actionunban = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -X DELETE -H 'Content-Type: application/json' -d '{"name":"<name>","ip":"<ip>"}' http://localhost/ban | ||||||
|  | [Init] | ||||||
|  | protocol = tcp | ||||||
|  | actiontype = <multiport> | ||||||
|  | allports = any | ||||||
|  | multiport = any port {<port>} | ||||||
|  | jail2ban_sock = /var/run/pf2ban/jail2ban.sock | ||||||
|  | jail2ban_user = login as set in password file for jail2ban | ||||||
|  | jail2ban_pass = password as set in password file for jail2ban | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | * Configure jail.local | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | cat <<'EOT' | tee /usr/local/etc/fail2ban/jail.local > /dev/null | ||||||
|  | [DEFAULT] | ||||||
|  | banaction = jail2ban-pf | ||||||
|  | ``` | ||||||
| @ -1,11 +1,16 @@ | |||||||
|  | ''' | ||||||
|  | jail2ban, a remote fail2ban action plugin using OpenBSD pf(8) | ||||||
|  | ''' | ||||||
|  | from ipaddress import ip_address | ||||||
|  | import re | ||||||
|  | from subprocess import CalledProcessError | ||||||
|  |  | ||||||
| from flask import Flask, request, jsonify, current_app | from flask import Flask, request, jsonify, current_app | ||||||
| from flask_httpauth import HTTPBasicAuth | from flask_httpauth import HTTPBasicAuth | ||||||
| from werkzeug.security import check_password_hash | from werkzeug.security import check_password_hash | ||||||
| from ipaddress import ip_address |  | ||||||
| import re |  | ||||||
| from jail2ban.pfctl import pfctl_table_op, pfctl_cfg_read, pfctl_cfg_write | from jail2ban.pfctl import pfctl_table_op, pfctl_cfg_read, pfctl_cfg_write | ||||||
| from jail2ban.auth import get_users | from jail2ban.auth import get_users | ||||||
| from subprocess import CalledProcessError |  | ||||||
|  |  | ||||||
|  |  | ||||||
| auth = HTTPBasicAuth() | auth = HTTPBasicAuth() | ||||||
| @ -15,6 +20,13 @@ PAT_PORT = r'^any(?:\s+port\s+{\w+(?:,\w+)*})?$' | |||||||
| PAT_PROT = r'^(?:tcp|udp)$' | PAT_PROT = r'^(?:tcp|udp)$' | ||||||
| PAT_NAME = r'^[\w\-]+$' | PAT_NAME = r'^[\w\-]+$' | ||||||
|  |  | ||||||
|  | _PFCTL_TABLE_PAT = r'''\s+(?P<addr>\S+)\n | ||||||
|  | \s+Cleared:\s+(?P<date>\S+\s+\S+\s+\d+\s+(?:\d{2}:){2}\d{2}\s+\d{4})\n | ||||||
|  | \s+In/Block:\s+\[\s+Packets:\s+(?P<in_pckt_block>\d+)\s+Bytes:\s+(?P<in_bytes_block>\d+)\s+\]\n | ||||||
|  | \s+In/Pass:\s+\[\s+Packets:\s+(?P<in_pckt_pass>\d+)\s+Bytes:\s+(?P<in_bytes_pass>\d+)\s+\]\n | ||||||
|  | \s+Out/Block:\s+\[\s+Packets:\s+(?P<out_pckt_block>\d+)\s+Bytes:\s+(?P<out_bytes_block>\d+)\s+\]\n | ||||||
|  | \s+Out/Pass:\s+\[\s+Packets:\s+(?P<out_pckt_pass>\d+)\s+Bytes:\s+(?P<out_bytes_pass>\d+)\s+\]''' | ||||||
|  |  | ||||||
|  |  | ||||||
| def untaint(pattern, string): | def untaint(pattern, string): | ||||||
|     ''' |     ''' | ||||||
| @ -23,11 +35,13 @@ def untaint(pattern, string): | |||||||
|     match = re.match(pattern, string) |     match = re.match(pattern, string) | ||||||
|     if match: |     if match: | ||||||
|         return match.string |         return match.string | ||||||
|     else: |  | ||||||
|     raise ValueError(f'"{string}" is tainted') |     raise ValueError(f'"{string}" is tainted') | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_app(): | def create_app(): | ||||||
|  |     ''' | ||||||
|  |     Create wsgi application instance | ||||||
|  |     ''' | ||||||
|     app = Flask(__name__, instance_relative_config=True) |     app = Flask(__name__, instance_relative_config=True) | ||||||
|  |  | ||||||
|     # load the instance config, if it exists, when not testing |     # load the instance config, if it exists, when not testing | ||||||
| @ -41,7 +55,17 @@ def create_app(): | |||||||
|         if username in users and \ |         if username in users and \ | ||||||
|                 check_password_hash(users.get(username), password): |                 check_password_hash(users.get(username), password): | ||||||
|             return username |             return username | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     @app.route("/ping", methods=['GET']) | ||||||
|  |     @auth.login_required | ||||||
|  |     def ping(): | ||||||
|  |         remote_user = auth.username() | ||||||
|  |         app.logger.info('Received ping for' | ||||||
|  |                         f' anchor f2b-jail/{remote_user}') | ||||||
|  |         return jsonify({'anchor': f'f2b-jail/{remote_user}', | ||||||
|  |                         'operation': 'ping', | ||||||
|  |                         'result': 'pong'}) | ||||||
|     @app.route("/flush/<name>", methods=['GET']) |     @app.route("/flush/<name>", methods=['GET']) | ||||||
|     @auth.login_required |     @auth.login_required | ||||||
|     def flush(name): |     def flush(name): | ||||||
| @ -57,6 +81,36 @@ def create_app(): | |||||||
|                         'operation': 'flush', |                         'operation': 'flush', | ||||||
|                         'result': [x.decode('ascii') for x in res]}) |                         'result': [x.decode('ascii') for x in res]}) | ||||||
|  |  | ||||||
|  |     @app.route("/list/<name>", methods=['GET']) | ||||||
|  |     @auth.login_required | ||||||
|  |     def list_table(name): | ||||||
|  |         remote_user = auth.username() | ||||||
|  |         name = untaint(PAT_NAME, name) | ||||||
|  |         app.logger.info(f'Flushing table f2b-{name}' | ||||||
|  |                         f' in anchor f2b-jail/{remote_user}') | ||||||
|  |         reply = {'anchor': f'f2b-jail/{remote_user}', | ||||||
|  |                         'table': f'f2b-{name}', | ||||||
|  |                         'operation': 'list'} | ||||||
|  |         try: | ||||||
|  |             res = pfctl_table_op('f2b-jail/{remote_user}', | ||||||
|  |                                  table='f2b-{name}', | ||||||
|  |                                  operation='show', | ||||||
|  |                                  verbose=True) | ||||||
|  |         except CalledProcessError as err: | ||||||
|  |             if err.stderr.find(b'pfctl: Table does not exist.') > 0: | ||||||
|  |                 res = [] | ||||||
|  |                 reply.update({'error': f'\'{name}\' is not a known fail2ban jail'}) | ||||||
|  |             else: | ||||||
|  |                 raise err | ||||||
|  |  | ||||||
|  |         result = [entry.groupdict() for entry in | ||||||
|  |                   re.finditer(_PFCTL_TABLE_PAT, | ||||||
|  |                               '\n'.join([x.decode('ascii') for x in res]), | ||||||
|  |                               re.MULTILINE|re.VERBOSE)] | ||||||
|  |         reply.update({'result': result}) | ||||||
|  |  | ||||||
|  |         return jsonify(reply), 200 if len(res) else 404 | ||||||
|  |  | ||||||
|     @app.route("/register", methods=['PUT', 'DELETE']) |     @app.route("/register", methods=['PUT', 'DELETE']) | ||||||
|     @auth.login_required |     @auth.login_required | ||||||
|     def register(): |     def register(): | ||||||
| @ -77,7 +131,7 @@ def create_app(): | |||||||
|                 ]) |                 ]) | ||||||
|             res = pfctl_cfg_write(f'f2b-jail/{remote_user}', |             res = pfctl_cfg_write(f'f2b-jail/{remote_user}', | ||||||
|                                   b'\n'.join(cfg) + b'\n') |                                   b'\n'.join(cfg) + b'\n') | ||||||
|         elif request.method == 'DELETE': |         else:  # 'DELETE': | ||||||
|             cfg = [cfg_line for cfg_line in cfg |             cfg = [cfg_line for cfg_line in cfg | ||||||
|                    if cfg_line.find(bytes(f'<f2b-{name}>', 'ascii')) == -1] |                    if cfg_line.find(bytes(f'<f2b-{name}>', 'ascii')) == -1] | ||||||
|             res = pfctl_cfg_write(f'f2b-jail/{remote_user}', |             res = pfctl_cfg_write(f'f2b-jail/{remote_user}', | ||||||
| @ -102,21 +156,21 @@ def create_app(): | |||||||
|         data = request.get_json() |         data = request.get_json() | ||||||
|         # name / ip |         # name / ip | ||||||
|         name = untaint(PAT_NAME, data['name']) |         name = untaint(PAT_NAME, data['name']) | ||||||
|         ip = ip_address(data['ip']) |         ip_addr = ip_address(data['ip']) | ||||||
|         if request.method == 'PUT': |         if request.method == 'PUT': | ||||||
|             app.logger.info(f'Add {ip} to f2b-{name}' |             app.logger.info(f'Add {ip_addr} to f2b-{name}' | ||||||
|                             f' in anchor f2b-jail/{remote_user}') |                             f' in anchor f2b-jail/{remote_user}') | ||||||
|             res = pfctl_table_op(f'f2b-jail/{remote_user}', |             res = pfctl_table_op(f'f2b-jail/{remote_user}', | ||||||
|                                  table=f'f2b-{name}', |                                  table=f'f2b-{name}', | ||||||
|                                  operation='add', |                                  operation='add', | ||||||
|                                  value=str(ip)) |                                  value=str(ip_addr)) | ||||||
|         elif request.method == 'DELETE': |         else:  # 'DELETE': | ||||||
|             app.logger.info(f'Remove {ip} from f2b-{name}' |             app.logger.info(f'Remove {ip_addr} from f2b-{name}' | ||||||
|                             f' in anchor f2b-jail/{remote_user}') |                             f' in anchor f2b-jail/{remote_user}') | ||||||
|             res = pfctl_table_op(f'f2b-jail/{remote_user}', |             res = pfctl_table_op(f'f2b-jail/{remote_user}', | ||||||
|                                  table=f'f2b-{name}', |                                  table=f'f2b-{name}', | ||||||
|                                  operation='delete', |                                  operation='delete', | ||||||
|                                  value=str(ip)) |                                  value=str(ip_addr)) | ||||||
|         return jsonify({'anchor': f'f2b-jail/{remote_user}', |         return jsonify({'anchor': f'f2b-jail/{remote_user}', | ||||||
|                         'table': f'f2b-{name}', |                         'table': f'f2b-{name}', | ||||||
|                         'operation': 'add' if request.method == 'PUT' |                         'operation': 'add' if request.method == 'PUT' | ||||||
| @ -133,6 +187,16 @@ def create_app(): | |||||||
|  |  | ||||||
|     @app.errorhandler(CalledProcessError) |     @app.errorhandler(CalledProcessError) | ||||||
|     def subprocess_err(error): |     def subprocess_err(error): | ||||||
|  |         ''' | ||||||
|  |         Show a json parsable error if the value is illegal | ||||||
|  |         ''' | ||||||
|  |         app.logger.fatal(error) | ||||||
|  |         app.logger.fatal('stdout: %s', error.stderr) | ||||||
|  |         app.logger.fatal('stderr: %s', error.stderr) | ||||||
|  |         return jsonify({'error': str(error)}), 500 | ||||||
|  |  | ||||||
|  |     @app.errorhandler(FileNotFoundError) | ||||||
|  |     def filenotfound_err(error): | ||||||
|         ''' |         ''' | ||||||
|         Show a json parsable error if the value is illegal |         Show a json parsable error if the value is illegal | ||||||
|         ''' |         ''' | ||||||
|  | |||||||
| @ -1,18 +1,15 @@ | |||||||
| from flask import current_app, g | from flask import current_app | ||||||
| import os |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_users(): | def get_users(): | ||||||
|     if 'users' not in g: |  | ||||||
|     users = {} |     users = {} | ||||||
|     authfile = current_app.config['AUTHFILE'] |     authfile = current_app.config['AUTHFILE'] | ||||||
|  |  | ||||||
|     current_app.logger.debug('Reading %s for users', authfile) |     current_app.logger.debug('Reading %s for users', authfile) | ||||||
|  |  | ||||||
|         with current_app.open_resource(os.path.join("..", |     with current_app.open_resource(authfile) as f: | ||||||
|                                                     authfile)) as f: |  | ||||||
|         for entry in f: |         for entry in f: | ||||||
|                 users.update({tuple(entry.decode('ascii').strip().split(':', 1))}) |             users.update({ | ||||||
|             g.users = users |                 tuple(entry.decode('ascii').strip().split(':', 1))}) | ||||||
|     current_app.logger.debug(g.users) |     current_app.logger.debug(users) | ||||||
|     return g.users |     return users | ||||||
|  | |||||||
| @ -1,11 +1,16 @@ | |||||||
|  | ''' | ||||||
|  | Lowlevel routines for calling the pf binary with passwordless sudo | ||||||
|  | ''' | ||||||
| import logging | import logging | ||||||
| from subprocess import run | from subprocess import run | ||||||
|  |  | ||||||
| _SUDO = '/usr/local/bin/sudo' | _SUDO = '/usr/local/bin/sudo' | ||||||
| _PFCTL = '/sbin/pfctl' | _PFCTL = '/sbin/pfctl' | ||||||
|  |  | ||||||
|  |  | ||||||
| def pfctl_cfg_read(anchor): | def pfctl_cfg_read(anchor): | ||||||
|  |     ''' | ||||||
|  |     Read pf rules stored under a certain anchor | ||||||
|  |     ''' | ||||||
|     cmd = [_SUDO, _PFCTL, '-a', anchor, '-sr'] |     cmd = [_SUDO, _PFCTL, '-a', anchor, '-sr'] | ||||||
|     logging.info('Running %s', cmd) |     logging.info('Running %s', cmd) | ||||||
|  |  | ||||||
| @ -16,6 +21,9 @@ def pfctl_cfg_read(anchor): | |||||||
|  |  | ||||||
|  |  | ||||||
| def pfctl_cfg_write(anchor, cfg): | def pfctl_cfg_write(anchor, cfg): | ||||||
|  |     ''' | ||||||
|  |     Write pf rules under a certain anchor | ||||||
|  |     ''' | ||||||
|     cmd = [_SUDO, _PFCTL, '-a', anchor, '-f-'] |     cmd = [_SUDO, _PFCTL, '-a', anchor, '-f-'] | ||||||
|     logging.info('Running %s', cmd) |     logging.info('Running %s', cmd) | ||||||
|     logging.info('Config %s', cfg) |     logging.info('Config %s', cfg) | ||||||
| @ -30,10 +38,14 @@ def pfctl_cfg_write(anchor, cfg): | |||||||
|  |  | ||||||
|  |  | ||||||
| def pfctl_table_op(anchor, **kwargs): | def pfctl_table_op(anchor, **kwargs): | ||||||
|  |     ''' | ||||||
|  |     pf table operation | ||||||
|  |     ''' | ||||||
|     table = kwargs['table'] |     table = kwargs['table'] | ||||||
|     operation = kwargs['operation'] |     operation = kwargs['operation'] | ||||||
|     value = kwargs['value'] if 'value' in kwargs else None |     value = kwargs['value'] if 'value' in kwargs else None | ||||||
|     cmd = [_SUDO, _PFCTL, '-a', anchor, '-t', table, '-T', operation, value] |     verbose =  '-v' if 'verbose' in kwargs and kwargs['verbose'] else None | ||||||
|  |     cmd = [_SUDO, _PFCTL, '-a', anchor, '-t', table, verbose, '-T', operation, value] | ||||||
|  |  | ||||||
|     logging.info('Running %s', cmd) |     logging.info('Running %s', cmd) | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import pytest | import pytest | ||||||
|  | import base64 | ||||||
| from jail2ban import create_app | from jail2ban import create_app | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -8,7 +9,7 @@ def app(): | |||||||
|     app.config.update({ |     app.config.update({ | ||||||
|         "TESTING": True, |         "TESTING": True, | ||||||
|         "SECRET_KEY": 'Testing', |         "SECRET_KEY": 'Testing', | ||||||
|         "AUTHFILE": 'tests/users-test.txt' |         "AUTHFILE": '../tests/users-test.txt' | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     # other setup can go here |     # other setup can go here | ||||||
| @ -26,3 +27,8 @@ def client(app): | |||||||
| @pytest.fixture() | @pytest.fixture() | ||||||
| def runner(app): | def runner(app): | ||||||
|     return app.test_cli_runner() |     return app.test_cli_runner() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture() | ||||||
|  | def valid_credentials(): | ||||||
|  |     return base64.b64encode(b"test.example.com:testpassword").decode("utf-8") | ||||||
|  | |||||||
| @ -1,8 +1,7 @@ | |||||||
| import base64 |  | ||||||
| from types import SimpleNamespace | from types import SimpleNamespace | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_ban_ipv6(client, mocker): | def test_ban_ipv6(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = SimpleNamespace() |     run_res = SimpleNamespace() | ||||||
| @ -13,16 +12,16 @@ def test_ban_ipv6(client, mocker): | |||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     json_payload = {"name": "sshd", "ip": "2001:db8::abad:cafe"} |     json_payload = {"name": "sshd", "ip": "2001:db8::abad:cafe"} | ||||||
|     response = client.put("/ban", |     response = client.put("/ban", | ||||||
|                           json=json_payload, |                           json=json_payload, | ||||||
|                           headers={"Authorization": "Basic " + valid_credentials}) |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert response.json['operation'] == 'add' |     assert response.json['operation'] == 'add' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_ban_ipv4(client, mocker): | def test_ban_ipv4(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = SimpleNamespace() |     run_res = SimpleNamespace() | ||||||
| @ -33,15 +32,16 @@ def test_ban_ipv4(client, mocker): | |||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     json_payload = {"name": "sshd", "ip": "192.0.2.42"} |     json_payload = {"name": "sshd", "ip": "192.0.2.42"} | ||||||
|     response = client.put("/ban", |     response = client.put("/ban", | ||||||
|                           json=json_payload, |                           json=json_payload, | ||||||
|                           headers={"Authorization": "Basic " + valid_credentials}) |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert response.json['operation'] == 'add' |     assert response.json['operation'] == 'add' | ||||||
|  |  | ||||||
| def test_ban_invalid(client, mocker): |  | ||||||
|  | def test_ban_invalid(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = SimpleNamespace() |     run_res = SimpleNamespace() | ||||||
| @ -52,16 +52,17 @@ def test_ban_invalid(client, mocker): | |||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     json_payload = {"name": "sshd", "ip": "not:an::addr:ess"} |     json_payload = {"name": "sshd", "ip": "not:an::addr:ess"} | ||||||
|     response = client.put("/ban", |     response = client.put("/ban", | ||||||
|                           json=json_payload, |                           json=json_payload, | ||||||
|                           headers={"Authorization": "Basic " + valid_credentials}) |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert response.json['error'] == "'not:an::addr:ess' does not appear to be an IPv4 or IPv6 address" |     assert response.json['error'] == "'not:an::addr:ess' does not " \ | ||||||
|  |                                      "appear to be an IPv4 or IPv6 address" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unban_ipv6(client, mocker): | def test_unban_ipv6(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = SimpleNamespace() |     run_res = SimpleNamespace() | ||||||
| @ -72,16 +73,16 @@ def test_unban_ipv6(client, mocker): | |||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     json_payload = {"name": "sshd", "ip": "2001:db8::abad:cafe"} |     json_payload = {"name": "sshd", "ip": "2001:db8::abad:cafe"} | ||||||
|     response = client.delete("/ban", |     response = client.delete("/ban", | ||||||
|                              json=json_payload, |                              json=json_payload, | ||||||
|                              headers={"Authorization": "Basic " + valid_credentials}) |                              headers={"Authorization": | ||||||
|  |                                       "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert response.json['operation'] == 'delete' |     assert response.json['operation'] == 'delete' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unban_ipv4(client, mocker): | def test_unban_ipv4(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = SimpleNamespace() |     run_res = SimpleNamespace() | ||||||
| @ -92,10 +93,10 @@ def test_unban_ipv4(client, mocker): | |||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     json_payload = {"name": "sshd", "ip": "192.0.2.42"} |     json_payload = {"name": "sshd", "ip": "192.0.2.42"} | ||||||
|     response = client.delete("/ban", |     response = client.delete("/ban", | ||||||
|                              json=json_payload, |                              json=json_payload, | ||||||
|                           headers={"Authorization": "Basic " + valid_credentials}) |                              headers={"Authorization": | ||||||
|  |                                       "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert response.json['operation'] == 'delete' |     assert response.json['operation'] == 'delete' | ||||||
|  | |||||||
| @ -1,9 +1,8 @@ | |||||||
| import base64 |  | ||||||
| from types import SimpleNamespace | from types import SimpleNamespace | ||||||
| from subprocess import CalledProcessError | from subprocess import CalledProcessError | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_flush(client, mocker): | def test_flush(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = SimpleNamespace() |     run_res = SimpleNamespace() | ||||||
| @ -14,25 +13,65 @@ def test_flush(client, mocker): | |||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     name = 'sshd' |     name = 'sshd' | ||||||
|     response = client.get(f"/flush/{name}", |     response = client.get(f"/flush/{name}", | ||||||
|                           headers={"Authorization": "Basic " + valid_credentials}) |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert response.json['operation'] == 'flush' |     assert response.json['operation'] == 'flush' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_flush_nonexistent(client, mocker): | def test_flush_nonexistent(client, mocker, valid_credentials): | ||||||
|  |  | ||||||
|     cmd = ['/usr/local/bin/sudo', '/sbin/pfctl', '-a', 'some/anchor', '-t', 'nonexistent', '-T', 'flush'] |     cmd = ['/usr/local/bin/sudo', | ||||||
|  |            '/sbin/pfctl', '-a', 'some/anchor', | ||||||
|  |            '-t', 'nonexistent', '-T', 'flush'] | ||||||
|  |  | ||||||
|  |     side_effect = CalledProcessError(255, cmd, output=b'', | ||||||
|  |                                      stderr=b'pfctl: Table does not exist') | ||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', |     mocker.patch('jail2ban.pfctl.run', | ||||||
|                  side_effect=CalledProcessError(255, cmd, output=b'', |                  side_effect=side_effect) | ||||||
|                                                 stderr=b'pfctl: Table does not exist')) |  | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     name = 'nonexistent' |     name = 'nonexistent' | ||||||
|     response = client.get(f"/flush/{name}", |     response = client.get(f"/flush/{name}", | ||||||
|                           headers={"Authorization": "Basic " + valid_credentials}) |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert 'error' in response.json |     assert 'error' in response.json | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_wrong_method(client, mocker, valid_credentials): | ||||||
|  |  | ||||||
|  |     cmd = ['/usr/local/bin/sudo', | ||||||
|  |            '/sbin/pfctl', '-a', 'some/anchor', | ||||||
|  |            '-t', 'nonexistent', '-T', 'flush'] | ||||||
|  |  | ||||||
|  |     side_effect = CalledProcessError(255, cmd, output=b'', | ||||||
|  |                                      stderr=b'pfctl: Table does not exist') | ||||||
|  |  | ||||||
|  |     mocker.patch('jail2ban.pfctl.run', | ||||||
|  |                  side_effect=side_effect) | ||||||
|  |  | ||||||
|  |     name = 'nonexistent' | ||||||
|  |     response = client.put(f"/flush/{name}", | ||||||
|  |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 405 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_filenotfound(app, mocker, valid_credentials): | ||||||
|  |  | ||||||
|  |     app.config.update({ | ||||||
|  |         "AUTHFILE": '../tests/nonexistent-users-test.txt' | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     client = app.test_client() | ||||||
|  |  | ||||||
|  |     name = 'nonexistent' | ||||||
|  |     response = client.get(f"/flush/{name}", | ||||||
|  |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 500 | ||||||
|  | |||||||
							
								
								
									
										140
									
								
								tests/test_list.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								tests/test_list.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,140 @@ | |||||||
|  | ''' | ||||||
|  | Tests for /list route | ||||||
|  | ''' | ||||||
|  | from types import SimpleNamespace | ||||||
|  | from subprocess import CalledProcessError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _PF_TABLE_LIST = b'''   192.0.2.66 | ||||||
|  |         Cleared:     Sat Jan  7 12:50:36 2023 | ||||||
|  |         In/Block:    [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |         In/Pass:     [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |         Out/Block:   [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |         Out/Pass:    [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |    2001:db8::abad:cafe | ||||||
|  |         Cleared:     Sat Jan  7 05:13:53 2023 | ||||||
|  |         In/Block:    [ Packets: 4                  Bytes: 240                ] | ||||||
|  |         In/Pass:     [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |         Out/Block:   [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |         Out/Pass:    [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |    2001:db8::abad:f00d:cafe | ||||||
|  |         Cleared:     Sat Jan  7 05:05:16 2023 | ||||||
|  |         In/Block:    [ Packets: 48                 Bytes: 2880               ] | ||||||
|  |         In/Pass:     [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |         Out/Block:   [ Packets: 0                  Bytes: 0                  ] | ||||||
|  |         Out/Pass:    [ Packets: 0                  Bytes: 0                  ]''' | ||||||
|  |  | ||||||
|  | _LIST_RESULT = [{'addr': '192.0.2.66', | ||||||
|  |   'date': 'Sat Jan  7 12:50:36 2023', | ||||||
|  |   'in_pckt_block': '0', | ||||||
|  |   'in_bytes_block': '0', | ||||||
|  |   'in_pckt_pass': '0', | ||||||
|  |   'in_bytes_pass': '0', | ||||||
|  |   'out_pckt_block': '0', | ||||||
|  |   'out_bytes_block': '0', | ||||||
|  |   'out_pckt_pass': '0', | ||||||
|  |   'out_bytes_pass': '0'}, | ||||||
|  |  {'addr': '2001:db8::abad:cafe', | ||||||
|  |   'date': 'Sat Jan  7 05:13:53 2023', | ||||||
|  |   'in_pckt_block': '4', | ||||||
|  |   'in_bytes_block': '240', | ||||||
|  |   'in_pckt_pass': '0', | ||||||
|  |   'in_bytes_pass': '0', | ||||||
|  |   'out_pckt_block': '0', | ||||||
|  |   'out_bytes_block': '0', | ||||||
|  |   'out_pckt_pass': '0', | ||||||
|  |   'out_bytes_pass': '0'}, | ||||||
|  |  {'addr': '2001:db8::abad:f00d:cafe', | ||||||
|  |   'date': 'Sat Jan  7 05:05:16 2023', | ||||||
|  |   'in_pckt_block': '48', | ||||||
|  |   'in_bytes_block': '2880', | ||||||
|  |   'in_pckt_pass': '0', | ||||||
|  |   'in_bytes_pass': '0', | ||||||
|  |   'out_pckt_block': '0', | ||||||
|  |   'out_bytes_block': '0', | ||||||
|  |   'out_pckt_pass': '0', | ||||||
|  |   'out_bytes_pass': '0'}] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_list_single_table(client, mocker, valid_credentials): | ||||||
|  |     ''' | ||||||
|  |     List a single pf table using the fail2ban jail name | ||||||
|  |     ''' | ||||||
|  |  | ||||||
|  |     def noop(): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     run_res = SimpleNamespace() | ||||||
|  |     run_res.stdout = _PF_TABLE_LIST | ||||||
|  |     run_res.stderr = b'No ALTQ support in kernel\nALTQ related functions disabled\n' | ||||||
|  |     run_res.returncode = 0 | ||||||
|  |     run_res.check_returncode = noop | ||||||
|  |  | ||||||
|  |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|  |     response = client.get("/list/sshd", | ||||||
|  |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|  |     assert response.json['table'] == 'f2b-sshd' | ||||||
|  |     assert response.json['result'] == _LIST_RESULT | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_list_nonexistent_table(client, mocker, valid_credentials): | ||||||
|  |     ''' | ||||||
|  |     Test for nonexistent table. Should result in a 404 not found | ||||||
|  |     ''' | ||||||
|  |     def noop(): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     run_res = SimpleNamespace() | ||||||
|  |     run_res.stdout = b'' | ||||||
|  |     run_res.stderr = b'No ALTQ support in kernel\nALTQ related functions disabled\n' \ | ||||||
|  |                      b'pfctl: Table does not exist.\n' | ||||||
|  |     run_res.returncode = 255 | ||||||
|  |     run_res.check_returncode = noop | ||||||
|  |  | ||||||
|  |     mocker.patch('jail2ban.pfctl.run', | ||||||
|  |                  return_value=run_res, | ||||||
|  |                  side_effect=CalledProcessError(run_res.returncode, | ||||||
|  |                                                 'foobar', | ||||||
|  |                                                  output=run_res.stdout, | ||||||
|  |                                                  stderr=run_res.stderr) | ||||||
|  |                  ) | ||||||
|  |  | ||||||
|  |     response = client.get("/list/nonexistent", | ||||||
|  |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 404 | ||||||
|  |     assert response.json['error'] == "'nonexistent' is not " \ | ||||||
|  |                                      "a known fail2ban jail" | ||||||
|  |  | ||||||
|  | def test_list_wrong_table_name(client, mocker, valid_credentials): | ||||||
|  |     ''' | ||||||
|  |     Test for an wrong table name that lets pfctl fail. should result in a 500 | ||||||
|  |     ''' | ||||||
|  |     def noop(): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     run_res = SimpleNamespace() | ||||||
|  |     run_res.stdout = b'' | ||||||
|  |     run_res.stderr = b'No ALTQ support in kernel\nALTQ related functions disabled\n' \ | ||||||
|  |             b'pfctl: Invalid argument.\n' | ||||||
|  |     run_res.returncode = 255 | ||||||
|  |     run_res.check_returncode = noop | ||||||
|  |  | ||||||
|  |     mocker.patch('jail2ban.pfctl.run', | ||||||
|  |                  return_value=run_res, | ||||||
|  |                  side_effect=CalledProcessError(run_res.returncode, | ||||||
|  |                                                 'foobar', | ||||||
|  |                                                  output=run_res.stdout, | ||||||
|  |                                                  stderr=run_res.stderr) | ||||||
|  |                  ) | ||||||
|  |  | ||||||
|  |     response = client.get("/list/notanerrorbuttestneedstofail", | ||||||
|  |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 500 | ||||||
|  |     assert response.json['error'] == "Command 'foobar' returned non-zero exit status 255." | ||||||
							
								
								
									
										10
									
								
								tests/test_ping.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/test_ping.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | def test_ping(client, mocker, valid_credentials): | ||||||
|  |     ''' | ||||||
|  |     Test application health check | ||||||
|  |     ''' | ||||||
|  |  | ||||||
|  |     response = client.get("/ping", | ||||||
|  |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|  |     assert response.json['operation'] == 'ping' | ||||||
| @ -1,4 +1,3 @@ | |||||||
| import base64 |  | ||||||
| from subprocess import CompletedProcess | from subprocess import CompletedProcess | ||||||
|  |  | ||||||
| pfctl_stdout_lines = b''' | pfctl_stdout_lines = b''' | ||||||
| @ -7,7 +6,12 @@ block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtps | |||||||
| block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtp | block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtp | ||||||
| block drop quick proto tcp from <f2b-sshd> to any port = ssh | block drop quick proto tcp from <f2b-sshd> to any port = ssh | ||||||
| block drop quick proto tcp from <f2b-recidive> to any | block drop quick proto tcp from <f2b-recidive> to any | ||||||
| ''' | '''.strip() + b'\n' | ||||||
|  |  | ||||||
|  | pfctl_stdout_lines_scratch = b'table <f2b-dovecot> persist counters\n' \ | ||||||
|  |                              b'block quick proto tcp from <f2b-dovecot>' \ | ||||||
|  |                              b' to any port ' \ | ||||||
|  |                              b'{pop3,pop3s,imap,imaps,submission,465,sieve}\n' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_register_unauth(client): | def test_register_unauth(client): | ||||||
| @ -19,7 +23,7 @@ def test_register_unauth(client): | |||||||
|     assert response.json['error'] == 'Access Denied' |     assert response.json['error'] == 'Access Denied' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_register_valid(client, mocker): | def test_unregister_valid(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = CompletedProcess(args=['true'], returncode=0) |     run_res = CompletedProcess(args=['true'], returncode=0) | ||||||
| @ -28,40 +32,67 @@ def test_register_valid(client, mocker): | |||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     json_payload = {"port": |     json_payload = {"port": | ||||||
|                     "any port {pop3,pop3s,imap,imaps,submission,465,sieve}", |                     "any port {pop3,pop3s,imap,imaps,submission,465,sieve}", | ||||||
|                     "name": "dovecot", "protocol": "tcp"} |                     "name": "dovecot", "protocol": "tcp"} | ||||||
|  |  | ||||||
|     response = client.delete("/register", |     response = client.delete("/register", | ||||||
|                              json=json_payload, |                              json=json_payload, | ||||||
|                              headers={"Authorization": "Basic " + valid_credentials}) |                              headers={"Authorization": | ||||||
|  |                                       "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert response.json['action'] == 'stop' |     assert response.json['action'] == 'stop' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unregister_valid(client, mocker): | def test_register_valid(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = CompletedProcess(args=['true'], returncode=0) |     run_res = CompletedProcess(args=['true'], returncode=0) | ||||||
|     run_res.stdout = pfctl_stdout_lines |     run_res.stdout = pfctl_stdout_lines | ||||||
|     run_res.check_returncode = noop |     run_res.check_returncode = noop | ||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     pfctl_run = mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     json_payload = {"port": |     json_payload = {"port": | ||||||
|                     "any port {pop3,pop3s,imap,imaps,submission,465,sieve}", |                     "any port {pop3,pop3s,imap,imaps,submission,465,sieve}", | ||||||
|                     "name": "dovecot", "protocol": "tcp"} |                     "name": "dovecot", "protocol": "tcp"} | ||||||
|  |  | ||||||
|     response = client.put("/register", |     response = client.put("/register", | ||||||
|                           json=json_payload, |                           json=json_payload, | ||||||
|                           headers={"Authorization": "Basic " + valid_credentials}) |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|  |     pfctl_run_input_arg = pfctl_run.call_args_list[1][1]['input'] | ||||||
|  |     for existing_line in pfctl_stdout_lines.splitlines(): | ||||||
|  |         assert existing_line in pfctl_run_input_arg.splitlines() | ||||||
|  |  | ||||||
|     assert response.json['action'] == 'start' |     assert response.json['action'] == 'start' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_register_invalid(client, mocker): | def test_register_valid_from_scratch(client, mocker, valid_credentials): | ||||||
|  |     def noop(): | ||||||
|  |         pass | ||||||
|  |     run_res = CompletedProcess(args=['true'], returncode=0) | ||||||
|  |     run_res.stdout = b'' | ||||||
|  |     run_res.check_returncode = noop | ||||||
|  |  | ||||||
|  |     pfctl_run = mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|  |     json_payload = {"port": | ||||||
|  |                     "any port {pop3,pop3s,imap,imaps,submission,465,sieve}", | ||||||
|  |                     "name": "dovecot", "protocol": "tcp"} | ||||||
|  |  | ||||||
|  |     response = client.put("/register", | ||||||
|  |                           json=json_payload, | ||||||
|  |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|  |     pfctl_run_input_arg = pfctl_run.call_args_list[1][1]['input'] | ||||||
|  |     assert pfctl_run_input_arg == pfctl_stdout_lines_scratch | ||||||
|  |     assert response.json['action'] == 'start' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_register_invalid(client, mocker, valid_credentials): | ||||||
|     def noop(): |     def noop(): | ||||||
|         pass |         pass | ||||||
|     run_res = CompletedProcess(args=['true'], returncode=0) |     run_res = CompletedProcess(args=['true'], returncode=0) | ||||||
| @ -70,13 +101,13 @@ def test_register_invalid(client, mocker): | |||||||
|  |  | ||||||
|     mocker.patch('jail2ban.pfctl.run', return_value=run_res) |     mocker.patch('jail2ban.pfctl.run', return_value=run_res) | ||||||
|  |  | ||||||
|     valid_credentials = base64.b64encode(b"test.example.com:testpassword").decode("utf-8") |  | ||||||
|     json_payload = {"port": |     json_payload = {"port": | ||||||
|                     "not a pf statement", |                     "not a pf statement", | ||||||
|                     "name": "dovecot", "protocol": "tcp"} |                     "name": "dovecot", "protocol": "tcp"} | ||||||
|  |  | ||||||
|     response = client.put("/register", |     response = client.put("/register", | ||||||
|                           json=json_payload, |                           json=json_payload, | ||||||
|                           headers={"Authorization": "Basic " + valid_credentials}) |                           headers={"Authorization": | ||||||
|  |                                    "Basic " + valid_credentials}) | ||||||
|  |  | ||||||
|     assert response.json['error'] == '"not a pf statement" is tainted' |     assert response.json['error'] == '"not a pf statement" is tainted' | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user