Saturday, May 09, 2026 AM03:21:24 HKT

This commit is contained in:
2026-05-09 03:21:32 +08:00
commit 41f17b127c
884 changed files with 263824 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2022 wargio <deroad@libero.it>
# SPDX-License-Identifier: LGPL-3.0-only
set -e
NGINX_VERSION="$1"
N_CPUS=$(nproc)
if [ -z "$NGINX_VERSION" ]; then
echo "usage: $0 <nginx version>"
echo "example: $0 1.12.2"
exit 1
fi
echo "############################"
echo " NGINX VERSION: $NGINX_VERSION"
echo "############################"
NEW_BUILD=true
if [ -d "nginx-tmp" ] && [ "$NGINX_VERSION" == $(cat nginx-tmp/nginx.version) ]; then
NEW_BUILD=false
fi
if $NEW_BUILD ; then
rm -rf nginx-source nginx-tmp nginx.tar.gz 2>&1 > /dev/null
wget --no-clobber -O nginx.tar.gz "https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz"
mkdir -p nginx-source nginx-tmp/naxsi_ut/root
echo "$NGINX_VERSION" > nginx-tmp/nginx.version
tar -C nginx-source -xzf nginx.tar.gz --strip-components=1
rm nginx.tar.gz
fi
export NAXSI_SRC_PATH=$(realpath naxsi_src/)
export NAXSI_TMP_PATH=$(realpath nginx-tmp/)
export NGINX_TMP_PATH=$(realpath nginx-source/)
if $NEW_BUILD ; then
cd "$NGINX_TMP_PATH"
./configure --with-cc-opt='-g -O2 -Wextra -Wall -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' \
--with-ld-opt='-Wl,-z,relro -Wl,-z,now -fPIC' \
--with-select_module \
--conf-path="$NAXSI_TMP_PATH/naxsi_ut/nginx.conf" \
--http-client-body-temp-path="$NAXSI_TMP_PATH/naxsi_ut/body/" \
--http-fastcgi-temp-path="$NAXSI_TMP_PATH/naxsi_ut/fastcgi/" \
--http-proxy-temp-path="$NAXSI_TMP_PATH/naxsi_ut/proxy/" \
--lock-path="$NAXSI_TMP_PATH/nginx.lock" \
--pid-path="$NAXSI_TMP_PATH/naxsi_ut/nginx.pid" \
--modules-path="$NAXSI_TMP_PATH/naxsi_ut/modules/" \
--without-mail_pop3_module \
--without-mail_smtp_module \
--without-mail_imap_module \
--with-http_realip_module \
--with-http_v2_module \
--without-http_uwsgi_module \
--without-http_scgi_module \
--prefix="$NAXSI_TMP_PATH/" \
--add-dynamic-module="$NAXSI_SRC_PATH" \
--error-log-path="$NAXSI_TMP_PATH/naxsi_ut/error.log" \
--conf-path="$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
cd ..
fi
make -C "$NGINX_TMP_PATH" -j$N_CPUS install
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2022 wargio <deroad@libero.it>
# SPDX-License-Identifier: LGPL-3.0-only
set -e
N_DEBUGS=$(cat naxsi_src/naxsi.h | grep "#define _debug" | grep 1 | wc -l)
if [ $N_DEBUGS -gt 0 ]; then
cat naxsi_src/naxsi.h | grep "#define _debug" | grep 1
exit 1
fi
+53
View File
@@ -0,0 +1,53 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2023 wargio <deroad@libero.it>
# SPDX-License-Identifier: LGPL-3.0-only
set -e
check_file_for() {
local FILENAME="$1"
local SEARCH_FOR="$2"
local EXPECTED="$3"
local FOUND=$(cat "$FILENAME" | grep "$SEARCH_FOR")
if [ -z "$FOUND" ]; then
echo "'$FILENAME' does not contain '$SEARCH_FOR'."
exit 1
fi
local HAS_EXPECTED=$(echo "$FOUND" | grep "$EXPECTED")
if [ -z "$HAS_EXPECTED" ]; then
echo "'$FILENAME' differs with what is expected to have."
echo "Expected: $EXPECTED"
echo "Got: $FOUND"
exit 1
fi
}
# check distro release version.
NAXSI_VERSION=$(grep -R "NAXSI_VERSION" naxsi_src/ | cut -d '"' -f2)
if [ -z "$NAXSI_VERSION" ]; then
echo "Cannot find NAXSI_VERSION in naxsi_src/"
exit 1
fi
check_file_for "distros/arch/PKGBUILD" "pkgver=" "$NAXSI_VERSION"
check_file_for "distros/alpine/1.18.0-r13/APKBUILD" "_add_module \"http-naxsi" "$NAXSI_VERSION"
check_file_for "distros/alpine/1.20.1-r3/APKBUILD" "_add_module \"http-naxsi" "$NAXSI_VERSION"
check_file_for "distros/alpine/1.20.2-r1/APKBUILD" "_add_module \"http-naxsi" "$NAXSI_VERSION"
check_file_for "distros/alpine/1.22.1-r0/APKBUILD" "_add_module \"http-naxsi" "$NAXSI_VERSION"
check_file_for "distros/alpine/1.18.0-r15/APKBUILD" "_add_module \"http-naxsi" "$NAXSI_VERSION"
check_file_for "distros/alpine/1.20.2-r0/APKBUILD" "_add_module \"http-naxsi" "$NAXSI_VERSION"
check_file_for "distros/alpine/1.22.0-r1/APKBUILD" "_add_module \"http-naxsi" "$NAXSI_VERSION"
check_file_for "distros/alpine/1.24.0-r3/APKBUILD" "_add_module \"http-naxsi" "$NAXSI_VERSION"
# check libinjection hash
SUBMODULE_HASH=$(git -C naxsi_src/libinjection rev-parse HEAD)
for DISTRO_YAML_HASH in $(grep "git -C naxsi_src/libinjection checkout" .github/workflows/distros.yaml | xargs -L1 echo | cut -d ' ' -f5); do
if [ "$DISTRO_YAML_HASH" != "$SUBMODULE_HASH" ]; then
echo "The libinjection hash in .github/workflows/distros.yaml does not match the submodule one."
echo "Expected: $SUBMODULE_HASH"
echo "Got: $DISTRO_YAML_HASH"
echo ".github/workflows/distros.yaml:"
grep -n "$DISTRO_YAML_HASH" .github/workflows/distros.yaml
exit 1
fi
done
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2022 wargio <deroad@libero.it>
# SPDX-License-Identifier: LGPL-3.0-only
RULE_FOLDER="$1"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
TMP_DIR="$SCRIPT_DIR/.tmp"
RET_VAL=0
if [ -z "$RULE_FOLDER" ]; then
echo "usage: $0 <rule folder>"
echo "example: $0 naxsi_rules/blocking"
exit 1
fi
if [[ -d "$TMP_DIR" ]]; then
rm -rf "$TMP_DIR"
fi
mkdir "$TMP_DIR" || exit 1
for FILE in $(ls "$RULE_FOLDER/"*.rules); do
FILENAME=$(basename $FILE)
echo "Linter: $FILE"
python "$SCRIPT_DIR/naxsi-lint.py" -r "$FILE" -o "$TMP_DIR/$FILENAME" || exit 1
DIFF=$(diff -u "$FILE" "$TMP_DIR/$FILENAME")
if [[ ! -z "$DIFF" ]]; then
diff -u --color=auto "$FILE" "$TMP_DIR/$FILENAME"
RET_VAL=1
fi
done
rm -rf "$TMP_DIR"
exit $RET_VAL
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2022 wargio <deroad@libero.it>
# SPDX-License-Identifier: LGPL-3.0-only
RUN_TEST="$1"
export NAXSI_CFG_PATH=$(realpath naxsi_rules/)
export NAXSI_TMP_PATH=$(realpath nginx-tmp/)
export NAXSI_TST_PATH=$(realpath unit-tests/)
if [ -z "$NAXSI_CFG_PATH" ] || [ -z "$NAXSI_TMP_PATH" ] || [ -z "$NAXSI_TST_PATH" ] ; then
echo "did you run first ci-build.sh ?"
exit 1
fi
echo "############################"
echo " running tests"
echo "############################"
cp -v "$NAXSI_TST_PATH/nginx-ci.conf" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
openssl req -batch -x509 -nodes -days 365 -newkey rsa:2048 -keyout "$NAXSI_TMP_PATH/nginx.key" -out "$NAXSI_TMP_PATH/nginx.crt"
export PATH="$NGINX_TMP_PATH/objs/:$PATH"
export TEST_NGINX_SERVROOT="$NAXSI_TMP_PATH/naxsi_ut/root"
export TEST_NGINX_BINARY="$NAXSI_TMP_PATH/sbin/nginx"
export TEST_NGINX_NAXSI_MODULE_SO="$NAXSI_TMP_PATH/naxsi_ut/modules/ngx_http_naxsi_module.so"
export TEST_NGINX_NAXSI_RULES="$NAXSI_CFG_PATH/naxsi_core.rules"
export TEST_NGINX_NAXSI_BLOCKING_RULES="$NAXSI_CFG_PATH/blocking"
export TEST_NGINX_NAXSI_WHITELISTS_RULES="$NAXSI_CFG_PATH/whitelists"
cd "$NAXSI_TMP_PATH"
if [ -z "$RUN_TEST" ]; then
prove -r "$NAXSI_TST_PATH/tests/"*.t
else
prove --verbose -r "$NAXSI_TST_PATH/tests/$RUN_TEST"
fi
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2022 wargio <deroad@libero.it>
# SPDX-License-Identifier: LGPL-3.0-only
RUN_TEST="$1"
if command -v python3 &> /dev/null ; then
PYTHON=python3
elif command -v python &> /dev/null ; then
PYTHON=python
else
echo "Cannot find python.."
exit 1
fi
export NAXSI_CFG_PATH=$(realpath naxsi_rules/)
export NAXSI_TMP_PATH=$(realpath nginx-tmp/)
export NAXSI_TST_PATH=$(realpath unit-tests/)
if [ -z "$NAXSI_CFG_PATH" ] || [ -z "$NAXSI_TMP_PATH" ] || [ -z "$NAXSI_TST_PATH" ] ; then
echo "did you run first ci-build.sh ?"
exit 1
fi
echo "############################"
echo " running tests"
echo "############################"
cp -v "$NAXSI_TST_PATH/nginx-ci.conf" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
openssl req -batch -x509 -nodes -days 365 -newkey rsa:2048 -keyout "$NAXSI_TMP_PATH/nginx.key" -out "$NAXSI_TMP_PATH/nginx.crt"
$PYTHON .scripts/naxsi-gen-tests.py
$PYTHON -m unittest discover ./unit-tests/python/ $RUN_TEST -v
+116
View File
@@ -0,0 +1,116 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2023 wargio <deroad@libero.it>
# SPDX-License-Identifier: LGPL-3.0-only
set -e
WAFEFFICACY_VERSION="v1.0"
NUCLEI_VERSION="2.9.6"
case "$1" in
"core" | "complete")
;;
*)
if [ -z "$1" ]; then
echo "usage: $0 [mode]"
else
echo "error: invalid mode option '$1'."
fi
echo "supported modes:"
echo " - core test efficacy only against naxsi_core.rules"
echo " - complete test efficacy against all rules"
exit 1
;;
esac
export NAXSI_CFG_PATH=$(realpath naxsi_rules)
export NAXSI_TMP_PATH=$(realpath nginx-tmp)
export NAXSI_DST_PATH=$(realpath distros/nginx)
export NAXSI_TST_PATH=$(realpath unit-tests/)
export NAXSI_VERSION=$(grep -R "NAXSI_VERSION" naxsi_src/ | cut -d '"' -f2)
export NAXSI_MODE="$1"
check_for() {
if [ "$1" == "f" ] && [ ! -f "$2" ]; then
echo "file '$1' does not exist!"
exit 1
elif [ "$1" == "d" ] && [ ! -d "$2" ]; then
echo "directory '$1' does not exist!"
exit 1
fi
}
if [ -z "$NAXSI_CFG_PATH" ] || [ -z "$NAXSI_TMP_PATH" ] || [ -z "$NAXSI_DST_PATH" ] ; then
echo "did you run first ci-build.sh ?"
exit 1
fi
if [ ! -d "$NAXSI_TMP_PATH/wafefficacy" ]; then
mkdir -p "$NAXSI_TMP_PATH/wafefficacy-tmp"
OLDPATH="$PWD"
cd "$NAXSI_TMP_PATH/wafefficacy-tmp"
# install wafefficacy
wget -O wafefficacy.zip "https://github.com/wargio/wafefficacy/releases/download/$WAFEFFICACY_VERSION/wafefficacy-linux-amd64.zip"
unzip wafefficacy.zip
mv -v wafefficacy-release "$NAXSI_TMP_PATH/wafefficacy"
cd "$OLDPATH"
rm -rf "$NAXSI_TMP_PATH/wafefficacy-tmp"
fi
if [ ! -f "$NAXSI_TMP_PATH/sbin/nuclei" ]; then
mkdir -p "$NAXSI_TMP_PATH/nuclei-tmp"
OLDPATH="$PWD"
cd "$NAXSI_TMP_PATH/nuclei-tmp"
# install nuclei
wget -O nuclei.zip "https://github.com/projectdiscovery/nuclei/releases/download/v$NUCLEI_VERSION/nuclei_""$NUCLEI_VERSION""_linux_amd64.zip"
unzip nuclei.zip || sleep 0
mv -v nuclei "$NAXSI_TMP_PATH/sbin/"
cd "$OLDPATH"
rm -rf "$NAXSI_TMP_PATH/nuclei-tmp"
fi
echo "############################"
echo " running wafefficacy"
echo "############################"
if [ ! -d "$NAXSI_TMP_PATH/naxsi_ut/logs" ]; then
mkdir -p "$NAXSI_TMP_PATH/naxsi_ut/logs"
fi
WAFEFFICACY_NGINX_CONF=$(realpath "$NAXSI_TST_PATH/nginx-ci-wafefficacy.conf")
RULES_NGINX_NAXSI_BLOCKING=$(realpath "$NAXSI_CFG_PATH/blocking")
RULES_NGINX_NAXSI_CORE=$(realpath "$NAXSI_CFG_PATH/naxsi_core.rules")
RULES_NGINX_NAXSI_WHITELISTS=$(realpath "$NAXSI_CFG_PATH/whitelists")
TEST_NGINX_NAXSI_MODULE_SO=$(realpath "$NAXSI_TMP_PATH/naxsi_ut/modules/ngx_http_naxsi_module.so")
check_for "f" "$WAFEFFICACY_NGINX_CONF"
check_for "f" "$TEST_NGINX_NAXSI_MODULE_SO"
check_for "f" "$RULES_NGINX_NAXSI_CORE"
check_for "d" "$RULES_NGINX_NAXSI_WHITELISTS"
check_for "d" "$RULES_NGINX_NAXSI_BLOCKING"
cp -v "$WAFEFFICACY_NGINX_CONF" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
sed -i "s#@RULES_NGINX_NAXSI_CORE@#include $RULES_NGINX_NAXSI_CORE;#g" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
sed -i "s#@TEST_NGINX_NAXSI_MODULE_SO@#$TEST_NGINX_NAXSI_MODULE_SO#g" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
case "$1" in
"core")
sed -i "s#@RULES_NGINX_NAXSI_BLOCKING@##g" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
sed -i "s#@RULES_NGINX_NAXSI_WHITELISTS@##g" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
;;
"complete")
sed -i "s#@RULES_NGINX_NAXSI_BLOCKING@#include $RULES_NGINX_NAXSI_BLOCKING/*;#g" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
sed -i "s#@RULES_NGINX_NAXSI_WHITELISTS@#include $RULES_NGINX_NAXSI_WHITELISTS/*;#g" "$NAXSI_TMP_PATH/naxsi_ut/nginx.conf"
;;
*)
echo "error: invalid mode option '$1'."
exit 1
;;
esac
cd "$NAXSI_TMP_PATH/wafefficacy"
"$NAXSI_TMP_PATH/sbin/nginx" -p "$NAXSI_TMP_PATH/naxsi_ut/" &
PATH="$PATH:$NAXSI_TMP_PATH/sbin" ./run.sh -w "naxsi-$NAXSI_VERSION" -t localhost:4242
jobs -p | xargs kill
+30
View File
@@ -0,0 +1,30 @@
@echo off
rem SPDX-FileCopyrightText: 2022 Alex <alex@staticlibs.net>
rem SPDX-License-Identifier: LGPL-3.0-only
@echo on
set BAD_SLASH_SCRIPT_DIR=%~dp0
set SCRIPT_DIR=%BAD_SLASH_SCRIPT_DIR:\=/%
set NAXSI_DIR=%SCRIPT_DIR%..
set NW_DIR=%NAXSI_DIR%/../nginx-windows
set NGINX_VERSION=%1
pushd .. || exit /b 1
git clone --branch 1.22.1-1 https://github.com/noproxy-http/nginx-windows.git || exit /b 1
popd || exit /b 1
pushd "%NW_DIR%" || exit /b 1
git submodule update --init || exit /b 1
popd || exit /b 1
pushd "%NW_DIR%/sources/nginx" || exit /b 1
git checkout release-%NGINX_VERSION% || exit /b 1
popd || exit /b 1
robocopy %NAXSI_DIR%/naxsi_src %NW_DIR%/modules/naxsi_src /e /nfl /ndl /njh /njs /nc /ns /np
pushd "%NW_DIR%" || exit /b 1
scripts\build.bat "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat" nossl
popd || exit /b 1
+15
View File
@@ -0,0 +1,15 @@
@echo off
rem SPDX-FileCopyrightText: 2022 Alex <alex@staticlibs.net>
rem SPDX-License-Identifier: LGPL-3.0-only
@echo on
set BAD_SLASH_SCRIPT_DIR=%~dp0
set SCRIPT_DIR=%BAD_SLASH_SCRIPT_DIR:\=/%
set NAXSI_DIR=%SCRIPT_DIR%..
pushd "%NAXSI_DIR%" || exit /b 1
python .scripts/naxsi-windows-test-dist.py %NAXSI_DIR%/../nginx-windows/build/dist || exit /b 1
python .scripts/naxsi-gen-tests.py || exit /b 1
python -m unittest discover unit-tests/python/ -v || exit /b 1
popd || exit /b 1
+434
View File
@@ -0,0 +1,434 @@
# -* coding: utf-8
# SPDX-FileCopyrightText: 2022 Alex <alex@staticlibs.net>
# SPDX-License-Identifier: LGPL-3.0-only
import os, sys
from collections import namedtuple
from os import path, replace
import re
NginxTest = namedtuple("NginxTest", "filename name user_files main_config http_config config more_headers request raw_request curl curl_protocol curl_options error_code, error_log no_error_log response_body")
unique_fun_names = set()
def is_test_file(dirpath, filename):
if not filename.endswith(".t"):
return False
filepath = path.join(dirpath, filename)
return path.isfile(filepath)
def read_list_of_test_files(test_file_or_dir):
if path.isfile(test_file_or_dir):
return [test_file_or_dir]
elif path.isdir(test_file_or_dir):
lst = os.listdir(test_file_or_dir)
filtered = list(filter(lambda f: is_test_file(test_file_or_dir, f), lst))
filtered.sort()
return list(map(lambda f: path.join(test_file_or_dir, f), filtered))
else:
print("ERROR: tests not found in directory: [{}]".format(test_file_or_dir))
sys.exit(1)
def collect_section(lines, idx, dest):
i = idx + 1
while not lines[i].startswith("--- "):
dest.append(lines[i])
i += 1
return i
def trim_test_lines(lines):
last_nonempty_idx = len(lines)
for i in reversed(range(len(lines))):
if len(lines[i]) == 0:
last_nonempty_idx = i
else:
break
return lines[:last_nonempty_idx]
def parse_test(lines, test_file, line_num):
lines = trim_test_lines(lines)
if len(lines) == 0:
return None
filename = path.basename(test_file)
name = lines[0][4:].strip()
user_files = []
main_config = []
http_config = []
config = []
more_headers = []
request = []
raw_request = []
curl = False
curl_protocol = "http"
curl_options = ""
error_code = 0
error_log = []
no_error_log = []
response_body = []
idx = 1
if lines[idx].lstrip().startswith("--- user_files"):
idx = collect_section(lines, idx, user_files)
if lines[idx].lstrip().startswith("--- main_config"):
idx = collect_section(lines, idx, main_config)
if lines[idx].lstrip().startswith("--- main_config"):
idx = collect_section(lines, idx, main_config)
if lines[idx].lstrip().startswith("--- http_config"):
idx = collect_section(lines, idx, http_config)
if lines[idx].lstrip().startswith("--- user_files"):
idx = collect_section(lines, idx, user_files)
if lines[idx].lstrip().startswith("--- config"):
idx = collect_section(lines, idx, config)
if lines[idx].lstrip().startswith("--- more_headers"):
idx = collect_section(lines, idx, more_headers)
if lines[idx].lstrip().startswith("--- request"):
idx = collect_section(lines, idx, request)
if lines[idx].lstrip().startswith("--- raw_request"):
idx = collect_section(lines, idx, raw_request)
if lines[idx].lstrip().startswith("--- curl"):
curl = True
idx = idx + 1
if lines[idx].lstrip().startswith("--- curl_protocol:"):
curl_protocol = lines[idx].strip()[len("--- curl_protocol:"):]
idx = idx + 1
if lines[idx].lstrip().startswith("--- curl_options:"):
curl_options = lines[idx].strip()[len("--- curl_options:"):]
idx = idx + 1
if not lines[idx].lstrip().startswith("--- error_code: "):
print("ERROR: Cannot parse test defintion, file: [{}], line: [{}]".format(test_file, line_num + idx - 2))
sys.exit(1)
prefix_len = len("--- error_code:")
error_code = int(lines[idx].lstrip()[prefix_len:])
if idx < len(lines) - 2 and lines[idx + 1].lstrip().startswith("--- error_log"):
idx += 2
while idx < len(lines) and (not lines[idx].startswith("--- ") or lines[idx].startswith("=== ")):
error_log.append(lines[idx])
idx += 1
idx -= 1
if idx < len(lines) - 2 and lines[idx + 1].lstrip().startswith("--- no_error_log"):
idx += 2
while idx < len(lines) and (not lines[idx].startswith("--- ") or lines[idx].startswith("=== ")):
no_error_log.append(lines[idx])
idx += 1
idx -= 1
if idx < len(lines) - 1 and lines[idx + 1].lstrip().startswith("--- response_body"):
idx+=1
response_body.append(lines[idx][18:].strip())
return NginxTest(filename, name, user_files, main_config, http_config, config, more_headers, request, raw_request, curl, curl_protocol, curl_options, error_code, error_log, no_error_log, response_body)
def read_list_of_tests(test_file):
tests = []
with open(test_file, encoding="utf-8") as file:
data_reached = False
test_lines = []
line_num = 0
for line in file:
line_num += 1
stripped = line.rstrip()
if stripped.startswith("#"):
continue
if not data_reached:
if "__DATA__" == stripped:
data_reached = True
continue
if stripped.startswith("==="):
if len(test_lines) > 0:
test = parse_test(test_lines, test_file, line_num - len(test_lines))
if test is not None:
tests.append(test)
test_lines = []
test_lines.append(stripped)
if len(test_lines) > 0:
test = parse_test(test_lines, test_file, line_num - len(test_lines))
if test is not None:
tests.append(test)
return tests
def format_array_eval_line(line):
line = line[2:-2].strip()
elems = line.split(",")
for i in range(len(elems)):
elems[i] = elems[i].strip()
xidx = elems[i].find("\"x")
if xidx > 0:
base = elems[i][1:xidx]
count = int(elems[i][xidx+2:])
elems[i] = ""
for j in range(count):
elems[i] += base
else:
elems[i] = elems[i][1:-1]
return "".join(elems)
def format_test_lines(test):
for i in range(len(test.http_config)):
test.http_config[i] = test.http_config[i].replace("\\", "\\\\")
test.http_config[i] = " {}".format(test.http_config[i])
for i in range(len(test.config)):
test.config[i] = " {}".format(test.config[i])
request_lines_to_pop = []
for i in range(len(test.request)):
line = test.request[i]
if line.lstrip().startswith("use "):
request_lines_to_pop.append(i)
if line.startswith("[[") and line.endswith("]]"):
test.request[i] = format_array_eval_line(line)
for idx in request_lines_to_pop:
test.request.pop(idx)
if len(test.request) > 0 and test.request[0].startswith("\""):
test.request[0] = test.request[0][1:]
test.request[-1] = test.request[-1][:-1]
for i in range(len(test.request)):
line = test.request[i]
test.request[i] = line.replace("\\\"", "\"").replace("\x00", "\\x00")
if len(test.raw_request) > 0 and test.raw_request[0].startswith("\""):
test.raw_request[0] = test.raw_request[0][1:]
test.raw_request[-1] = test.raw_request[-1][:-1]
for i in range(len(test.raw_request)):
line = test.raw_request[i]
test.raw_request[i] = line.replace("\\\"", "\"")
if len(test.error_log) > 0:
el = test.error_log
for i in range(len(el)):
if el[i].startswith("["):
el[i] = el[i][1:].lstrip()
if el[i].startswith("qr@"):
el[i] = el[i][3:].lstrip()
if el[i].endswith("]"):
el[i] = el[i][:-1].rstrip()
if el[i].endswith(","):
el[i] = el[i][:-1].rstrip()
if el[i].endswith("@"):
el[i] = el[i][:-1].rstrip()
el[i] = el[i].replace("server=localhost", "server=127.0.0.1")
if len(el[-1]) == 0:
el.pop()
if len(test.response_body) > 0:
for i in range(len(test.response_body)):
test.response_body[i] = test.response_body[i].replace("localhost", "127.0.0.1")
def hotpatch_test(test):
if "TEST 24: Testing MULTIPART POSTs" == test.name:
test.more_headers[1] = test.more_headers[1].replace(
"Content-Length: 355",
"Content-Length: 353",
)
def parse_req_method(req):
space_idx = req[0].find(" ")
return req[0][:space_idx]
def parse_req_data(req, method):
if method not in ["POST", "PATCH"]:
return None
rn_idx = req[0].find("\\r\\n")
inline = ""
if rn_idx > 0:
inline = req[0][rn_idx:]
res = inline
if len(req) > 1:
res += "\n".join(req[1:])
if res.startswith("\\r\\n"):
res = res[4:]
if res.endswith("\\r\\n\\r\\n"):
res = res[:-8]
return res
def parse_req_url(req):
space_idx = req[0].find(" ")
rn_idx = req[0].find("\\r\\n")
if rn_idx > 0:
return req[0][space_idx + 1:rn_idx]
else:
return req[0][space_idx + 1:]
def parse_headers(more_headers):
headers = {}
for line in more_headers:
idx = line.find(": ")
name = line[:idx]
value = line[idx + 2:]
headers[name] = value
return headers
def parse_user_files(user_files):
res = {}
name = None
for line in user_files:
stripped = line.strip()
if stripped.startswith(">>> "):
name = stripped[4:]
res[name] = ""
else:
res[name] += line
return res
def gen_file_header(test_file_name_noext):
cls_name = test_file_name_noext[2:].replace("-", "_")
return """# -* coding: utf-8
# SPDX-FileCopyrightText: NAXSI project
# SPDX-License-Identifier: LGPL-3.0-only
import unittest
from _test_utils import nginx_runner
class {}(unittest.TestCase):""".format(cls_name)
def gen_function_header(test):
fun_name = test.name.strip()
fun_name = re.sub(r'[^a-zA-Z\d_]', '_', fun_name)
if not fun_name.startswith("test_"):
fun_name = "test_{}".format(fun_name)
while fun_name in unique_fun_names:
fun_name += "_"
unique_fun_names.add(fun_name)
docstring = test.name.replace("\\x", "\\\\x")
return '''
def {}(self):
"""
{}
"""'''.format(fun_name, docstring)
def gen_runner_init(test):
res = ''' with nginx_runner('''
if len(test.http_config) > 0:
http_config = "\n".join(test.http_config)
res += '''
http_config="""
{}
""",'''.format(http_config)
if len(test.config) > 0:
config = "\n".join(test.config)
res += '''
config="""
{}
""",'''.format(config)
if len(test.user_files) > 0:
res += '''
user_files={'''
files = parse_user_files(test.user_files)
for name, value in files.items():
res += '''
"{}": "{}",'''.format(name, value)
res += '''
},'''
res += '''
) as nr:'''
return res;
def gen_request(test):
if len(test.response_body) > 0:
res = ''' ec, resp_body = nr.request('''
else:
res = ''' ec = nr.request('''
url = parse_req_url(test.request).replace("\"", "\\\"")
res += '''
url="{}",'''.format(url)
method = parse_req_method(test.request)
if "GET" != method:
res += '''
method="{}",'''.format(method)
if len(test.more_headers) > 0:
res += '''
headers={'''
headers = parse_headers(test.more_headers)
for name, value in headers.items():
res += '''
"{}": "{}",'''.format(name, value)
res += '''
},'''
data = parse_req_data(test.request, method)
if data is not None:
res += '''
data="""{}""",'''.format(data)
if test.curl:
res += '''
curl=True,'''
if len(test.curl_protocol) > 0:
res += '''
curl_protocol="{}",'''.format(test.curl_protocol)
if len(test.curl_options) > 0:
res += '''
curl_options="{}",'''.format(test.curl_options)
if len(test.response_body) > 0:
res += '''
resp_body_required=True'''
res += '''
)'''. format(test.error_code)
return res
def gen_raw_request(test):
raw_req = "\n".join(test.raw_request)
return ''' ec = nr.raw_request("""
{}""")'''.format(raw_req)
def gen_checks(test):
res = " self.assertEqual(ec, {})".format(test.error_code)
if len(test.error_log) > 0:
res += '''
elm = nr.error_log_matches(['''
for line in test.error_log:
line = line.replace("\"", "\\\"")
res += '''
r"{}",'''.format(line)
res += '''
])
self.assertTrue(elm)'''
if len(test.no_error_log) > 0:
res += '''
nelm = nr.error_log_matches(['''
for line in test.no_error_log:
line = line.replace("\"", "\\\"")
res += '''
r"{}",'''.format(line)
res += '''
])
self.assertFalse(nelm)'''
if len(test.response_body) > 0:
response_body = "\n".join(test.response_body)
res += '''
self.assertEqual(resp_body, """{}""".encode("utf-8"))'''.format(response_body)
return res
def gen_test_function(test):
header = gen_function_header(test)
runner = gen_runner_init(test)
if len(test.request) > 0:
send_req = gen_request(test)
else:
send_req = gen_raw_request(test)
checks = gen_checks(test)
return '''
{}
{}
{}
{}'''.format(header, runner, send_req, checks)
def write_python_test_file(test_file_name, test_list):
scripts_dir = path.dirname(__file__)
naxsi_dir = path.dirname(scripts_dir)
dest_dir = path.join(naxsi_dir, "unit-tests", "python")
test_file_name_noext = path.splitext(test_file_name)[0]
test_file_name_nodash = test_file_name_noext.replace("-", "_")
dest_file_name = "test_{}.py".format(test_file_name_nodash)
dest_file_path = path.join(dest_dir, dest_file_name)
if path.exists(dest_file_path):
os.remove(dest_file_path)
with open(dest_file_path, "w", encoding="utf-8") as file:
file.write(gen_file_header(test_file_name_noext))
for test in test_list:
hotpatch_test(test)
format_test_lines(test)
file.write(gen_test_function(test))
file.write("\n")
if __name__ == "__main__":
scripts_dir = path.dirname(__file__)
naxsi_dir = path.dirname(scripts_dir)
tests_dir = path.join(naxsi_dir, "unit-tests", "tests")
test_files_list = read_list_of_test_files(tests_dir)
for test_file_path in test_files_list[:]:
unique_fun_names.clear()
test_list = read_list_of_tests(test_file_path)
test_file_name = path.basename(test_file_path)
write_python_test_file(test_file_name, test_list[:])
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2022 deroad <wargio@libero.it>
# SPDX-License-Identifier: LGPL-3.0-only
import argparse
import sys
import re
import shlex
DESCRIPTION='Naxsi Rule Linter'
EPILOG='''
This tool lints naxsi rules
Example:
$ python naxsi-lint.py --rule /path/to/file.rules --output output.rules --begin-id 4200000
'''
def clean_token(token):
if not token.startswith('"'):
return token
return token[1:-1]
def find_keyword(keyword, tokens):
for token in tokens:
token = clean_token(token)
if token.startswith(keyword):
return token
return None
class Rule(object):
def __init__(self, tokens, line_num, comments):
super(Rule, self).__init__()
self.line_num = line_num
self.comments = comments
self.main_rule = find_keyword("MainRule", tokens)
self.basic_rule = find_keyword("BasicRule", tokens)
self.id = find_keyword("id:", tokens)
self.msg = find_keyword("msg:", tokens)
self.str = find_keyword("str:", tokens)
self.rx = find_keyword("rx:", tokens)
self.mz = find_keyword("mz:", tokens)
self.s = find_keyword("s:", tokens)
self.wl = find_keyword("wl:", tokens)
self.d = find_keyword("d:libinj_", tokens)
self.negative = find_keyword("negative", tokens)
# validate first
self.validate()
if self.wl != None:
ids = list(set(self.wl[3:].split(',')))
ids.sort()
self.wl = 'wl:' + ','.join(ids)
else:
self.id = int(self.id[3:])
if self.msg != None:
self.msg = re.sub(r'\s+', ' ', self.msg.strip())
self.msg = re.sub(r'"', '\'', self.msg.strip())
def validate(self):
if self.main_rule == None and self.basic_rule == None:
print("ERROR: {}: missing 'MainRule' or 'BasicRule' prefix".format(self.line_num))
sys.exit(1)
if self.main_rule != None and self.basic_rule != None:
print("ERROR: {}: 'MainRule' and 'BasicRule' are set".format(self.line_num))
sys.exit(1)
if self.id == None and self.wl == None:
print("ERROR: {}: missing 'id:' or 'wl:'".format(self.line_num))
sys.exit(1)
elif self.id != None and self.wl != None:
print("ERROR: {}: 'id:' and 'wl:' are both set".format(self.line_num))
sys.exit(1)
if self.is_whitelist():
if self.mz == None:
print("ERROR: {}: missing whitelist 'mz:'".format(self.line_num))
sys.exit(1)
elif self.str != None:
print("ERROR: {}: 'str:' is set for a whitelist".format(self.line_num))
sys.exit(1)
elif self.rx != None:
print("ERROR: {}: 'rx:' is set for a whitelist".format(self.line_num))
sys.exit(1)
elif self.d != None:
print("ERROR: {}: 'd:' is set for a whitelist".format(self.line_num))
sys.exit(1)
else:
self.match()
if self.mz == None:
print("ERROR: {}: missing rule 'mz:'".format(self.line_num))
sys.exit(1)
if self.s == None:
print("ERROR: {}: missing rule 's:'".format(self.line_num))
sys.exit(1)
def prefix(self):
return self.main_rule if self.main_rule != None else self.basic_rule
def match(self):
if self.rx != None:
return self.rx
elif self.str != None:
return self.str
elif self.d != None:
return self.d
print("ERROR: {}: missing rule 'rx:' or 'str:' or 'd:'".format(self.line_num))
sys.exit(1)
def is_whitelist(self):
return self.wl != None
def message(self):
if self.msg == None:
return "match " + self.match().replace('"', '')
return self.msg[4:]
def print(self):
if len(self.comments) > 0:
print('\n'.join(self.comments))
prefix = self.prefix()
args = []
if self.is_whitelist():
args = [
self.wl,
self.negative,
'"{}"'.format(self.mz),
'"{}"'.format(self.msg),
]
else:
args = [
'id:{}'.format(self.id),
'"{}"'.format(self.s),
self.negative,
'"{}"'.format(self.match()),
'"{}"'.format(self.mz),
'"{}"'.format(self.msg),
]
args = list(filter(lambda x: x != None and x != '"None"', args))
print("{} {};".format(self.prefix(), ' '.join(args)))
def parse_file(filename, rules, whitelists, ruleid):
lines = []
with open(filename, 'r') as fp:
lines = fp.readlines()
line_num = 0
comments = []
for line in lines:
line_num += 1
line = line.strip()
if line.startswith("#") or len(line) < 1:
comments.append(line)
continue
elif '#' in line and not '#"' in line and not line.endswith(';'):
comments.append('#' + line.split('#', 1)[-1])
line = line.split('#')[0]
if not line.endswith(';'):
print("ERROR: {}: missing ; at the end the line '{}'".format(line_num, line))
sys.exit(1)
line = re.sub(r';$', " ;", line)
tokens = shlex.split(line, posix=False)
# print("{}: {}".format(line_num, tokens))
rule = Rule(tokens, line_num, comments)
comments = []
if rule.is_whitelist():
whitelists.append(rule)
continue
if ruleid > 100:
rule.id = ruleid
ruleid += 1
elif rule.id in rules:
print("ERROR: {}: Rule {} already exists at line {}".format(line_num, rule.id, rules[rule.id].line_num))
sys.exit(1)
rules[rule.id] = rule
def print_rules(rules_ids, rules, whitelists):
header = len(rules_ids) < 1
if len(whitelists) > 0:
for whitelist in whitelists:
if not header:
header = True
print('#############')
print("# Whitelist #")
print('#############')
whitelist.print()
for idx in rules_ids:
if header:
header = False
print('#########')
print("# Rules #")
print('#########')
rules[idx].print()
def print_translate_dictionary(rules_ids, rules, whitelists):
for idx in rules_ids:
print('"{}","{}"'.format(rules[idx].id, rules[idx].message()))
output_formats = {
'rules': print_rules,
'logstash_translate_dictionary': print_translate_dictionary,
}
def main():
parser = argparse.ArgumentParser(usage='%(prog)s [options]', description=DESCRIPTION, epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-r', '--rule', default='', help='source rule to parse')
parser.add_argument('-o', '--output', default='', help='path to the output file')
parser.add_argument('-f', '--format', default='rules', help='format of the output file ({})'.format(','.join(list(output_formats.keys()))))
parser.add_argument('-b', '--begin-id', default=0, type=int, help='rebase all rules ids from this id number (id must be > 100)')
args = parser.parse_args()
if len(sys.argv) == 1 or \
len(args.rule) < 1 or \
len(args.output) < 1 or \
len(args.format) < 1 or \
args.format not in output_formats or \
(args.begin_id != 0 and args.begin_id <= 100):
parser.print_help(sys.stderr)
sys.exit(1)
rules = {}
whitelists = []
parse_file(args.rule, rules, whitelists, args.begin_id)
rules_ids = list(rules.keys())
rules_ids.sort()
file_format = output_formats[args.format]
fd = sys.stdout
if args.output != '-':
sys.stdout = open(args.output, 'w')
file_format(rules_ids, rules, whitelists)
if args.output != '-':
sys.stdout.close()
sys.stdout = fd
if __name__ == '__main__':
main()
+30
View File
@@ -0,0 +1,30 @@
# -* coding: utf-8
# SPDX-FileCopyrightText: 2022 Alex <alex@staticlibs.net>
# SPDX-License-Identifier: LGPL-3.0-only
import os, shutil, sys
from os import path
if __name__ == "__main__":
if 2 != len(sys.argv):
print("ERROR: The path to Nginx dist must be specified as a first and only argument")
sys.exit(1)
src = sys.argv[1]
if not path.isdir(src):
print("ERROR: Nginx dist not found on the specified path: [{}]".format(src))
sys.exit(1)
scripts_dir = path.dirname(__file__)
naxsi_dir = path.dirname(scripts_dir)
nginx_dir = path.join(naxsi_dir, "nginx-tmp")
if path.exists(nginx_dir):
shutil.rmtree(nginx_dir)
os.mkdir(nginx_dir)
shutil.copytree(path.join(src, "conf"), path.join(nginx_dir, "conf"))
os.remove(path.join(nginx_dir, "conf", "nginx.conf"))
shutil.copytree(path.join(src, "logs"), path.join(nginx_dir, "logs"))
shutil.copytree(path.join(src, "temp"), path.join(nginx_dir, "temp"))
os.mkdir(path.join(nginx_dir, "html"))
with open(path.join(nginx_dir, "html", "index.html"), "w", encoding="utf-8") as file:
file.write("<html>Hello Nginx!<html>")
shutil.copy(path.join(src, "nginx.exe"), nginx_dir)
print("Test Nginx dist prepared successfully");