more and more rules

This commit is contained in:
root
2023-03-23 11:37:27 +01:00
parent e86dfc5162
commit 1e466dfb45
4 changed files with 172 additions and 0 deletions

4
install.sh Executable file
View File

@ -0,0 +1,4 @@
pip install -r requirements.txt
mkdir -p /var/opt/waf
cp waf3.py /var/opt/waf/waf3.py
cp waf /usr/local/bin/waf

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
arrow
pyufw
requests
peewee
#playhouse.sqlite_ext
click
fabric

3
waf vendored Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
python3 /var/opt/waf/waf3.py "$@"

158
waf3.py Normal file
View File

@ -0,0 +1,158 @@
#!/usr/bin/env python3
import os
os.nice(20)
import asyncio, subprocess
import arrow
import peewee
import click
from path import Path
import pyufw as ufw
from playhouse.sqlite_ext import SqliteExtDatabase
now = arrow.utcnow()
last_hour = now.shift(hours=-1).floor('hour')
last_thirty_min = now.shift(minutes=-30)
# last_period = last_thirty_min
last_period = last_hour
vroots = Path("/srv")
logs = vroots.glob('*/logs/*access*.log')
whitelist_ips = [
'127.0.0.1',
'5.9.113.251',
'90.175.189.153',
'78.47.46.238',
]
db = SqliteExtDatabase('/var/opt/waf/waf.db', pragmas={'journal_mode': 'wal'})
class Attack(peewee.Model):
ip = peewee.CharField(unique=True)
host = peewee.CharField(index = True)
date = peewee.DateTimeField(default=now.datetime)
# date = peewee.DateTimeField(default=datetime.datetime.now)
count = peewee.BigIntegerField()
class Meta:
database = db
Attack.create_table(True)
@click.group()
def cli():
pass
async def nginx_reload():
returned_value = subprocess.call('/usr/bin/systemctl reload nginx', shell=True)
if returned_value == 0:
print('Nginx reloaded')
async def get_denied():
denied = []
for rule in ufw.get_rules().values():
if "deny from " in rule:
denied.append(rule.split(' ')[-1])
return denied
async def deny(ip):
ufw.add('deny from ' + ip + ' to any', number=1)
async def check(ip, host, date_position):
date = arrow.get(date_position,'DD/MMM/YYYY:HH:mm:ss')
if date > last_period:
find = Attack.get_or_none(Attack.ip == ip)
if find:
find.count = find.count + 1
find.save()
else:
data = {'ip': ip, 'date':date.datetime, 'host': host, 'count':1}
Attack.create(**data)
async def scan(log):
suspects = []
suspects_login = {}
suspects_404 = {}
for line in log.lines():
splitted = line.split()
ip = splitted[0].strip()
method = splitted[5].strip()[1:]
url = splitted[6].strip()
status = splitted[8].strip()
date_position = splitted[3][1:]
host = log.parent.parent.basename()
if ip not in whitelist_ips:
if url.startswith('/.') and '.well-known' not in url:
suspects.append(check(ip, host, date_position))
elif 'xmlrpc.php' in url:
suspects.append(check(ip, host, date_position))
elif 'login.php' in url and method == 'POST':
if ip in suspects_login:
suspects_login[ip].append( (ip, host, date_position) )
else:
suspects_login[ip] = [(ip, host, date_position),]
elif 'wp-admin' in url and status not in ['200','302','499']:
suspects.append(check(ip, host, date_position))
def is_suspicious_login(item):
return len(item[1]) > 2
filtered = dict(filter(is_suspicious_login, suspects_login.items()))
for ip,suspect in filtered.items():
suspects.append(check(ip, suspect[-1][1], suspect[-1][2]))
await asyncio.gather(*suspects)
async def block():
denied = await get_denied()
found = Attack.select().where(
(Attack.ip.not_in(denied)) &
(Attack.count > 7)
)
if found.count() > 0:
click.echo(click.style('New IPs to block: {}'.format(found.count()), fg="yellow"))
to_deny = []
for attack in found:
to_deny.append( deny(attack.ip) )
await asyncio.gather(*to_deny)
else:
click.echo(click.style('No IPs to block', fg="blue"))
def report():
click.echo(
click.style(
"Hosting logs: {}".format(len(logs)),
fg="cyan"
)
)
click.echo(
click.style(
"Attacks in database: {}".format( Attack.select().count() ),
fg="cyan"
)
)
async def start():
report()
scans = []
for log in logs:
scans.append(scan(log))
await asyncio.gather(*scans)
await block()
#print("Updated number of attacks in database: {}".format( Attack.select().count() ) )
@cli.command('purge')
def waf_purge():
click.echo('Clean all blocked IPs')
@cli.command('report')
def waf_report():
click.echo(click.style('Report WAF attacks', fg="blue", bold=True))
report()
@cli.command('scan')
def waf_scan():
before = arrow.utcnow()
click.echo(click.style('Scan', fg="blue", blink=True, bold=True))
# click.echo('Scan')
asyncio.run(start())
after = arrow.utcnow()
click.echo(click.style('Finished in {}'.format(after - before), fg="blue"))
if __name__ == '__main__':
cli()