/var/ossec/etc/rules/local_rules.xml 내용 추가<group name="modsecurity,attack,auto_block">
<rule id="100201" level="10">
<match>ModSecurity: Access denied</match>
<match>403</match>
<description>ModSecurity 403 access denied - auto block</description>
</rule>
</group>
/var/ossec/etc/ossec.conf 내용 추가<command>
<name>wazuh-nft-block</name>
<executable>wazuh-nft-block.sh</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<active-response>
<command>wazuh-nft-block</command>
<location>defined-agent</location>
<agent_id>alerts.json에서 확인되는 agent id</agent_id>
<rules_id>100201</rules_id>
<timeout>600</timeout>
</active-response>
수정 후 Manager, Agent 모두 재시작
#!/usr/bin/env bash
set -euo pipefail
# -------- Config --------
TABLE_FAMILY="inet"
TABLE_NAME="filter"
CHAIN_NAME="input"
SET_NAME="wazuh_block"
DEFAULT_TIMEOUT_SEC=600
# -------- Helpers --------
read_stdin_json() {
local s=""
# Read entire stdin (could be multi-line)
while IFS= read -r line; do
s+="$line"
done
printf "%s" "$s"
}
extract_json_value() {
# Usage: extract_json_value "$JSON" "key"
# Tries jq if available; otherwise uses a minimal sed parser for simple string fields.
local json="$1"
local key="$2"
if command -v jq >/dev/null 2>&1; then
# Many Wazuh AR JSON payloads are flat; support both top-level and nested "parameters" variants.
jq -r --arg k "$key" '
(.. | objects | select(has($k)) | .[$k]) // empty
' <<<"$json" | head -n1
else
# Minimal fallback: "key":"value"
echo "$json" | sed -nE 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -n1
fi
}
is_valid_ipv4() {
local ip="$1"
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
# ensure each octet <= 255
IFS='.' read -r o1 o2 o3 o4 <<<"$ip"
for o in "$o1" "$o2" "$o3" "$o4"; do
[[ "$o" -ge 0 && "$o" -le 255 ]] || return 1
done
return 0
}
nft_ensure_base() {
# Ensure table/chain/set and drop rule exist.
nft list table "$TABLE_FAMILY" "$TABLE_NAME" >/dev/null 2>&1 || nft add table "$TABLE_FAMILY" "$TABLE_NAME"
# Ensure input chain exists with hook (only if not already present)
if ! nft list chain "$TABLE_FAMILY" "$TABLE_NAME" "$CHAIN_NAME" >/dev/null 2>&1; then
nft add chain "$TABLE_FAMILY" "$TABLE_NAME" "$CHAIN_NAME" "{ type filter hook input priority 0; policy accept; }"
fi
# Ensure set exists (timeout flag is important)
if ! nft list set "$TABLE_FAMILY" "$TABLE_NAME" "$SET_NAME" >/dev/null 2>&1; then
nft add set "$TABLE_FAMILY" "$TABLE_NAME" "$SET_NAME" "{ type ipv4_addr; flags timeout; }"
fi
# Ensure drop rule referencing the set exists (insert near top)
if ! nft list chain "$TABLE_FAMILY" "$TABLE_NAME" "$CHAIN_NAME" | grep -q "ip saddr @$SET_NAME drop"; then
nft insert rule "$TABLE_FAMILY" "$TABLE_NAME" "$CHAIN_NAME" ip saddr "@$SET_NAME" drop
fi
}
nft_block_ip() {
local ip="$1"
local timeout_sec="$2"
nft_ensure_base
# Add with timeout. If already exists, ignore error.
nft add element "$TABLE_FAMILY" "$TABLE_NAME" "$SET_NAME" "{ $ip timeout ${timeout_sec}s }" 2>/dev/null || true
}
nft_unblock_ip() {
local ip="$1"
nft_ensure_base
nft delete element "$TABLE_FAMILY" "$TABLE_NAME" "$SET_NAME" "{ $ip }" 2>/dev/null || true
}
iptables_block_ip() {
local ip="$1"
# Insert only if not already present
iptables -C INPUT -s "$ip" -j DROP 2>/dev/null || iptables -I INPUT -s "$ip" -j DROP
}
iptables_unblock_ip() {
local ip="$1"
iptables -D INPUT -s "$ip" -j DROP 2>/dev/null || true
}
# -------- Main --------
JSON="$(read_stdin_json)"
[[ -n "${JSON:-}" ]] || exit 0
SRCIP="$(extract_json_value "$JSON" "srcip")"
ACTION="$(extract_json_value "$JSON" "command")"
# Some payloads use "action" instead of "command"
if [[ -z "${ACTION:-}" ]]; then
ACTION="$(extract_json_value "$JSON" "action")"
fi
# Some Wazuh payloads include timeout; if present use it, else default
TIMEOUT="$(extract_json_value "$JSON" "timeout")"
if [[ -z "${TIMEOUT:-}" ]]; then
TIMEOUT="$DEFAULT_TIMEOUT_SEC"
fi
# sanitize timeout
if ! [[ "$TIMEOUT" =~ ^[0-9]+$ ]]; then
TIMEOUT="$DEFAULT_TIMEOUT_SEC"
fi
# Must have a srcip to act on
if [[ -z "${SRCIP:-}" ]]; then
log "No srcip found in payload: $JSON"
exit 0
fi
# Only handle IPv4 here (can extend to IPv6 later)
if ! is_valid_ipv4 "$SRCIP"; then
log "Invalid IPv4 srcip: $SRCIP"
exit 0
fi
# Decide add vs remove
# Depending on Wazuh version/config, ACTION might be: "add", "delete", "remove", "stop", etc.
# We treat anything containing delete/remove/stop as unblock, otherwise block.
if echo "$JSON" | grep -Eqi '"(delete|remove|del|stop)"'; then
if command -v nft >/dev/null 2>&1; then
nft_unblock_ip "$SRCIP"
elif command -v iptables >/dev/null 2>&1; then
iptables_unblock_ip "$SRCIP"
fi
log "Unblocked $SRCIP"
else
if command -v nft >/dev/null 2>&1; then
nft_block_ip "$SRCIP" "$TIMEOUT"
elif command -v iptables >/dev/null 2>&1; then
iptables_block_ip "$SRCIP"
else
log "Neither nft nor iptables found; cannot block $SRCIP"
exit 0
fi
log "Blocked $SRCIP for ${TIMEOUT}s"
fi
exit 0
sudo install -m 750 -o root -g wazuh wazuh-nft-block.sh /var/ossec/active-response/bin/wazuh-nft-block.sh
sudo systemctl restart wazuh-agent