DEV Community

Carrie
Carrie

Posted on

Integrating Open Source WAF with Wazuh(Part 2)

This article is written by a SafeLine WAF user, 曼联小胖子

Part 1 is here.

Configuration Process

SafeLine WAF Configuration

Log in to the SafeLine WAF management interface using a local browser, and add the domain name to be protected according to your company’s actual situation, e.g., a.test.com:

Image description

Add a custom IP group for use in the blacklist later:

Image description

Add a blacklist and associate it with the IP group created in the previous step:
Image description

Configure the security functions of the SafeLine WAF according to actual needs:

Image description

SafeLine WAF Server Configuration

  1. Log in to the SafeLine WAF server, map the local login port 5432 of the SafeLine pgsql database to the host machine, as the subsequent shell scripts need to log in to the database:
docker stop safeline-pg
systemctl stop docker
vim /var/lib/docker/containers/$(docker ps --no-trunc | grep safeline-pg | awk '{print $1}')/hostconfig.json
# Modify PortBindings to the following configuration:
"PortBindings":{"5432/tcp":[{"HostIp":"127.0.0.1","HostPort":"5432"}]}
systemctl start docker
netstat -tnlp | grep 5432 # Check if the pgsql database port is successfully mapped to the host machine
Enter fullscreen mode Exit fullscreen mode
  1. Obtain pgsql Database Password:
cat /data/safeline/.env | grep POSTGRES_PASSWORD | tail -n 1 | awk -F '=' '{print $2}'
Enter fullscreen mode Exit fullscreen mode
  1. Create .pgpass to Pass Password to Shell Script:
vim /var/scripts/.pgpass
Add the following parameters:
localhost:5432:safeline-ce:safeline-ce:abcd  # Replace 'abcd' with the password obtained in step 2
Enter fullscreen mode Exit fullscreen mode
  1. Create Shell Script to Generate Syslog Logs for Wazuh Monitoring:
mkdir /var/log/waf_alert
touch /var/log/waf_alert/waf_alert.log
touch /var/scripts/waf_log.sh
chmod u+x /var/scripts/waf_log.sh
vim /var/scripts/waf_log.sh

Add the following code:

#!/bin/bash

# Set PGPASSFILE environment variable
export PGPASSFILE=/var/scripts/.pgpass

# PostgreSQL connection information
PG_HOST="localhost"
PORT="5432"
DATABASE="safeline-ce"
USERNAME="safeline-ce"
HOSTNAME=$(hostname)
PROGRAM_NAME="safeline-ce"

# Get the ID of the last WAF attack event log, stored in MGT_DETECT_LOG_BASIC table
LAST_ID=$(psql -h $PG_HOST -p $PORT -U $USERNAME -d $DATABASE -t -P footer=off -c "SELECT id FROM PUBLIC.MGT_DETECT_LOG_BASIC ORDER BY id desc limit 1")

while true; do
    # Fetch the latest WAF attack event log from pgsql database. If no new log is generated, this SQL will return empty
    raw_log=$(psql -h $PG_HOST -p $PORT -U $USERNAME -d $DATABASE -t -P footer=off -c "SELECT TO_CHAR(to_timestamp(timestamp) AT TIME ZONE 'Asia/Hong_Kong', 'Mon DD HH24:MI:SS'), CONCAT(PROVINCE, CITY) AS SRC_CITY, SRC_IP, CONCAT(HOST, ':', DST_PORT) AS HOST,url_path,rule_id,id FROM PUBLIC.MGT_DETECT_LOG_BASIC where id > '$LAST_ID' ORDER BY id asc limit 1")

    # Check the SQL query result. If there are new logs, perform the following operations: rewrite the SQL query result as syslog log and record it to /var/log/waf_alert/waf_alert.log
    if [ -n "$raw_log" ]; then
        ALERT_TIME=$(echo "$raw_log" | awk -F ' \\| ' '{print $1}')
        SRC_CITY=$(echo "$raw_log" | awk -F ' \\| ' '{print $2}')
        SRC_IP=$(echo "$raw_log" | awk -F ' \\| ' '{print $3}')
        DST_HOST=$(echo "$raw_log" | awk -F ' \\| ' '{print $4}')
        URL=$(echo "$raw_log" | awk -F ' \\| ' '{print $5}')
        RULE_ID=$(echo "$raw_log" | awk -F ' \\| ' '{print $6}')
        EVENT_ID=$(echo "$raw_log" | awk -F ' \\| ' '{print $7}')
        syslog="src_city:$SRC_CITY, src_ip:$SRC_IP, dst_host:$DST_HOST, url:$URL, rule_id:$RULE_ID, log_id:$EVENT_ID"
        echo $ALERT_TIME $HOSTNAME $PROGRAM_NAME: $syslog >> /var/log/waf_alert/waf_alert.log
        # Update the last processed event ID
        LAST_ID=$(($LAST_ID+1))
    fi
    sleep 3
done
Enter fullscreen mode Exit fullscreen mode
  1. Run Monitoring Script in Background and Add to Startup:
nohup /var/scripts/waf_log.sh > /dev/null 2>&1 &
vim /etc/rc.local

Add the following code:

nohup /var/scripts/waf_log.sh > /dev/null 2>&1 &
Enter fullscreen mode Exit fullscreen mode

Lark Configuration

  1. Add a security alert notification group and a group robot, which will be used to send alert cards to the group later.

Image description

  1. Choose Custom Robot

Image description

  1. Save webhook address that will be used when configure wazuh script.

Image description

Wazuh Server Configuration

Add a script to be called when an alert is triggered. There are two files: the custom-waf file does not need any modification.

touch /var/ossec/integrations/custom-waf
chmod 750 /var/ossec/integrations/custom-waf
chown root:wazuh /var/ossec/integrations/custom-waf
vim /var/ossec/integrations/custom-waf ,添加以下代码:

#!/bin/sh
# Copyright (C) 2015, Wazuh Inc.
# Created by Wazuh, Inc. <info@wazuh.com>.
# This program is free software; you can redistribute it and/or modify it under the terms of GPLv2

WPYTHON_BIN="framework/python/bin/python3"

SCRIPT_PATH_NAME="$0"

DIR_NAME="$(cd $(dirname ${SCRIPT_PATH_NAME}); pwd -P)"
SCRIPT_NAME="$(basename ${SCRIPT_PATH_NAME})"

case ${DIR_NAME} in
    */active-response/bin | */wodles*)
        if [ -z "${WAZUH_PATH}" ]; then
            WAZUH_PATH="$(cd ${DIR_NAME}/../..; pwd)"
        fi

        PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
    ;;
    */bin)
        if [ -z "${WAZUH_PATH}" ]; then
            WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
        fi

        PYTHON_SCRIPT="${WAZUH_PATH}/framework/scripts/${SCRIPT_NAME}.py"
    ;;
    */integrations)
        if [ -z "${WAZUH_PATH}" ]; then
            WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
        fi

        PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
    ;;
esac

${WAZUH_PATH}/${WPYTHON_BIN} ${PYTHON_SCRIPT} "$@"
Enter fullscreen mode Exit fullscreen mode
  1. The custom-waf.py script is used to block IPs and send Lark alerts. Modify the commented parts with your own information.
mkdir /var/log/waf/block_ip.log
chown wazuh:wazuh /var/log/waf/block_ip.log
chmod 644 /var/log/waf/block_ip.log
touch /var/ossec/integrations/custom-waf.py
chmod 750 /var/ossec/integrations/custom-waf.py
chown root:wazuh /var/ossec/integrations/custom-waf.py
vim /var/ossec/integrations/custom-waf.py
Enter fullscreen mode Exit fullscreen mode

Add the following code:

#!/usr/bin/env python
import sys
import json
import ssl
import requests
import os
import datetime
import urllib3
from urllib3.exceptions import InsecureRequestWarning
urllib3.disable_warnings(InsecureRequestWarning)

def read_alert_file():
    alert_file = open(sys.argv[1])
    alert_json = json.loads(alert_file.read())
    alert_file.close()
    timestamp = alert_json['predecoder']['timestamp']
    hostname = alert_json['predecoder']['hostname']
    description = alert_json['rule']['description']
    full_log = alert_json['full_log']
    src_city = alert_json['data']['src_city']
    src_ip = alert_json['data']['src_ip']
    dst_host = alert_json['data']['dst_host']
    dst_url = alert_json['data']['dst_url']
    print(src_ip)
    return timestamp,hostname,description,full_log,src_city,src_ip,dst_host,dst_url

def login(host,username,password):
    csrf_url = f"{host}/api/open/auth/csrf"
    response = requests.get(csrf_url, verify=False)
    data = response.json()
    csrf_token = data["data"]["csrf_token"]
    login_data = {
        'csrf_token': csrf_token,
        'username': username,
        'password': password,
    }
    login_url = f"{host}/api/open/auth/login"
    response = requests.post(login_url,json=login_data,verify=False)
    data = response.json()
    jwt = data["data"]["jwt"]
    return jwt

def get_info(host,jwt):
    url = f"{host}/api/open/ipgroup?top=1001"
    headers={
    "Content-Type": "application/json",
    "Authorization": f"Bearer {jwt}"
    }
    response = requests.get(url,headers=headers,verify=False)
    data = response.json()
    ip_group_id = data["data"]["nodes"][-1]["id"]
    ip_group_name = data["data"]["nodes"][-1]["comment"]
    ips = data["data"]["nodes"][-1]["ips"]
    ips_count = len(ips)
    url = f"{host}/api/open/rule"
    requests.get(url,headers=headers,verify=False)
    return ip_group_id,ip_group_name,ips,ips_count

def update_ip_group(host,jwt,ip_group_id,ip_group_name,ips,src_ip):
    url = f"{host}/api/open/ipgroup"
    ips.append(src_ip)
    headers={
        "Content-Type": "application/json",
        "Authorization": f"Bearer {jwt}"
    }
    body = {
        "id":ip_group_id,
        "reference":"",
        "comment":ip_group_name,
        "ips":ips
    }
    requests.put(url,json=body,headers=headers,verify=False)

def create_ip_group(host,jwt,ip_group_id,ip_group_name,src_ip):
    url = f"{host}/api/open/ipgroup"
    ip_group_id = ip_group_id +1
    ip_group_name = "black_ip_group_name-" + str(ip_group_id)
    src_ip = [src_ip]
    headers={
        "Content-Type": "application/json",
        "Authorization": f"Bearer {jwt}"
    }
    body = {
        "reference":"",
        "comment":ip_group_name,
        "ips":src_ip
    }
    requests.post(url,json=body,headers=headers,verify=False)
    return ip_group_id,ip_group_name

def create_rule(host,jwt,ip_group_id,ip_group_name):
    url = f"{host}/api/open/rule"
    headers={
        "Content-Type": "application/json",
        "Authorization": f"Bearer {jwt}"
    }
    body = {
        "action": 1,
        "comment": ip_group_name,
        "is_enabled": True,
        "pattern": [{
            "k": "src_ip",
            "op": "in",
            "v": str(ip_group_id),
            "sub_k": ""
        }]
    }
    requests.post(url,json=body,headers=headers,verify=False)

def feishu(webhook_url,timestamp,hostname,description,full_log,src_city,src_ip,dst_host,dst_url):
    headers={
        "Content-Type": "application/json"
    }
    msg_data = {
        "msg_type": "interactive",
        "card": {
            "header": {
                "title": {
                    "tag": "plain_text",
                    "content": description
                },
                "template": "red"
            },
            "elements": [
                {
                    "tag": "div",
                    "text": {
                        "tag": "lark_md",
                        "content": "**请注意:以下攻击源IP已加入黑名单。**" + "\n\n" + "**告警时间: **" + timestamp + "\n" + "**告警来源: **" + hostname + "\n" + "**攻击源地址: **" + src_city + "\n" + "**攻击源IP: **" + src_ip + "\n" + "**被攻击地址: **" + dst_host + "\n" + "**被攻击路径: **" + dst_url
                    }
                },
                {
                    "tag": "hr"
                },
                {
                    "tag": "div",
                    "text": {
                        "tag": "lark_md",
                        "content": "**原始syslog日志:**" + "\n" + full_log
                    }
                },
            ]
        }
    }
    requests.post(webhook_url,json=msg_data,headers=headers)

def print_log(log_file_path,src_ip):
    now = datetime.datetime.now()
    time_str = now.strftime('%b %d %H:%M:%S')
    log_template = "{time} prod-waf safe-line:{ip} has been blocked."
    message = log_template.format(time=time_str, ip=src_ip)
    log_file_path = log_file_path
    with open(log_file_path, 'a') as log_file:
        log_file.write(message + '\n')

def main(host,username,password,log_file_path,webhook_url):
    timestamp,hostname,description,full_log,src_city,src_ip,dst_host,dst_url = read_alert_file()
    jwt = login(host,username,password)
    ip_group_id,ip_group_name,ips,ips_count = get_info(host,jwt)
    if ips_count > 999:
        ip_group_id,ip_group_name = create_ip_group(host,jwt,ip_group_id,ip_group_name,src_ip)
        create_rule(host,jwt,ip_group_id,ip_group_name)
    else:
        update_ip_group(host,jwt,ip_group_id,ip_group_name,ips,src_ip)
    feishu(webhook_url,timestamp,hostname,description,full_log,src_city,src_ip,dst_host,dst_url)
    print_log(log_file_path,src_ip)

host = "https://192.168.1.1:9443" #替换成WAF地址
log_file_path = "/var/log/waf/block_ip.log"
webhook_url = "https://open.feishu.cn/open-apis/bot/v2/hook/c742cec0-94e9-449b-8473-597b873" #替换成飞书机器人地址
username = "admin"
password = "123456" #替换成WAF密码
if __name__ == "__main__":
    main(host,username,password,log_file_path,webhook_url)
    sys.exit(0)
Enter fullscreen mode Exit fullscreen mode
  1. Add Wazuh Server Decoder
touch /var/ossec/etc/decoders/safeline-waf-decoders.xml
chmod 660 /var/ossec/etc/decoders/safeline-waf-decoders.xml
chown wazuh:wazuh /var/ossec/etc/decoders/safeline-waf-decoders.xml
vim /var/ossec/etc/decoders/safeline-waf-decoders.xml
Enter fullscreen mode Exit fullscreen mode

Add the following code:

<decoder name="safeline-ce">
    <program_name>safeline-ce</program_name>
    <regex>src_city:(\.*), src_ip:(\.*), dst_host:(\.*), url:(\.*), rule_id:(\.*), log_id:(\d+)</regex>
    <order>src_city,src_ip,dst_host,dst_url,rule_id,log_id</order>
</decoder>
Enter fullscreen mode Exit fullscreen mode
  1. Add Wazuh Server Alert Rule
touch /var/ossec/etc/rules/safeline-waf-rules.xml
chmod 660 /var/ossec/etc/rules/safeline-waf-rules.xml
chown wazuh:wazuh /var/ossec/etc/rules/safeline-waf-rules.xml
vim /var/ossec/etc/rules/safeline-waf-rules.xml
Enter fullscreen mode Exit fullscreen mode

Add the following code:

<group name="syslog,safeline,">
    <rule id="119101" level="7">
        <decoded_as>safeline-ce</decoded_as>
        <match>a.test.com</match> <!-- Replace a.test.com with the domain protected by the WAF -->
        <description>Intrusion Event: a.test.com</description> <!-- You can modify this description to your preference; it will appear in the Feishu message card title -->
    </rule>
</group>
Enter fullscreen mode Exit fullscreen mode
  1. Modify Wazuh Server ossec Configuration
vim /var/ossec/etc/ossec.conf

Add the following code:

<integration>
    <name>custom-waf</name>
    <rule_id>119101</rule_id>
    <alert_format>json</alert_format>
</integration>
Enter fullscreen mode Exit fullscreen mode
  1. Restart Wazuh Server to Apply All Configurations
/var/ossec/bin/wazuh-control restart
Enter fullscreen mode Exit fullscreen mode

To be continued...

Top comments (0)