From 0cfa11e4caf1d6cd1402d3d4a2ea043d17d4d249 Mon Sep 17 00:00:00 2001 From: Max Leske <250711+theseion@users.noreply.github.com> Date: Tue, 24 Dec 2024 10:32:34 +0100 Subject: [PATCH 1/3] chore: remove some lingering stuff from openresty --- docker-bake.hcl | 8 -------- renovate.json | 12 ------------ 2 files changed, 20 deletions(-) diff --git a/docker-bake.hcl b/docker-bake.hcl index fbaf2e9..e35bf32 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -47,14 +47,6 @@ variable "lua-modules-debian" { ] } -variable "lua-modules-luarocks" { - default = [ - "lua-resty-openidc", - "lua-zlib", - "luasocket" - ] -} - variable "REPOS" { # List of repositories to tag default = [ diff --git a/renovate.json b/renovate.json index 73771f9..9d86c42 100644 --- a/renovate.json +++ b/renovate.json @@ -96,18 +96,6 @@ ], "depNameTemplate": "coreruleset/coreruleset", "datasourceTemplate": "github-releases" - }, - { - "description": "Docs: OpenResty", - "customType": "regex", - "fileMatch": [ - "^README\\.md$" - ], - "matchStrings": [ - "OpenResty (?\\d+\\.\\d+\\.\\d+(\\.\\d+)?)" - ], - "depNameTemplate": "openresty/openresty", - "datasourceTemplate": "docker" } ] } From 7adda108c81218702e1978fd2e7a2456ad279e83 Mon Sep 17 00:00:00 2001 From: Max Leske <250711+theseion@users.noreply.github.com> Date: Sun, 22 Dec 2024 15:26:03 +0100 Subject: [PATCH 2/3] feat: improve rule configuration - Configure rules to update in a separate configuration file - Use generic script to configure rules that doesn't rely on exact match of rule - Log which rules are configured - Log when a rule that should be configured wasn't found --- apache/Dockerfile | 3 +- apache/Dockerfile-alpine | 3 +- apache/docker-entrypoint.sh | 4 +- nginx/Dockerfile | 5 +- nginx/Dockerfile-alpine | 4 +- src/opt/modsecurity/activate-plugins.sh | 3 + src/opt/modsecurity/activate-rules.sh | 148 -------------------- src/opt/modsecurity/configure-rules.conf | 28 ++++ src/opt/modsecurity/configure-rules.sh | 171 +++++++++++++++++++++++ 9 files changed, 214 insertions(+), 155 deletions(-) delete mode 100755 src/opt/modsecurity/activate-rules.sh create mode 100644 src/opt/modsecurity/configure-rules.conf create mode 100755 src/opt/modsecurity/configure-rules.sh diff --git a/apache/Dockerfile b/apache/Dockerfile index e27a253..d4b05d9 100644 --- a/apache/Dockerfile +++ b/apache/Dockerfile @@ -158,7 +158,7 @@ COPY src/etc/modsecurity.d/*.conf /etc/modsecurity.d/ COPY src/bin/* /usr/local/bin/ COPY apache/conf/extra/*.conf /usr/local/apache2/conf/extra/ COPY src/etc/modsecurity.d/*.conf /etc/modsecurity.d/ -COPY src/opt/modsecurity/activate-*.sh /opt/modsecurity/ +COPY src/opt/modsecurity/* /opt/modsecurity/ COPY apache/docker-entrypoint.sh / RUN set -eux; \ @@ -167,6 +167,7 @@ RUN set -eux; \ apt-get install -qq -y --no-install-recommends --no-install-suggests \ ca-certificates \ curl \ + ed \ gnupg \ iproute2 \ libcurl3-gnutls \ diff --git a/apache/Dockerfile-alpine b/apache/Dockerfile-alpine index da97250..29225de 100644 --- a/apache/Dockerfile-alpine +++ b/apache/Dockerfile-alpine @@ -166,7 +166,7 @@ COPY --from=build /usr/local/apache2/ModSecurity-${MODSEC2_VERSION}/unicode.mapp COPY --from=crs_release /opt/owasp-crs /opt/owasp-crs COPY src/etc/modsecurity.d/*.conf /etc/modsecurity.d/ COPY src/bin/* /usr/local/bin/ -COPY src/opt/modsecurity/activate-*.sh /opt/modsecurity/ +COPY src/opt/modsecurity/* /opt/modsecurity/ COPY apache/conf/extra/*.conf /usr/local/apache2/conf/extra/ COPY apache/docker-entrypoint.sh / @@ -176,6 +176,7 @@ RUN set -eux; \ apk add --no-cache \ ca-certificates \ curl \ + ed \ gnupg \ iproute2 \ libfuzzy2 \ diff --git a/apache/docker-entrypoint.sh b/apache/docker-entrypoint.sh index 01dc372..a15b6be 100755 --- a/apache/docker-entrypoint.sh +++ b/apache/docker-entrypoint.sh @@ -3,7 +3,7 @@ /usr/local/bin/generate-certificate /usr/local/apache2 /usr/local/bin/check-low-port -. /opt/modsecurity/activate-plugins.sh -. /opt/modsecurity/activate-rules.sh +/opt/modsecurity/activate-plugins.sh +/opt/modsecurity/configure-rules.sh exec "$@" diff --git a/nginx/Dockerfile b/nginx/Dockerfile index addee0b..3b4c493 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -192,7 +192,6 @@ ENV \ SSL_VERIFY=off \ WORKER_CONNECTIONS=1024 \ # CRS specific variables - PARANOIA=1 \ ANOMALY_INBOUND=5 \ ANOMALY_OUTBOUND=4 \ BLOCKING_PARANOIA=1 @@ -208,7 +207,8 @@ COPY src/etc/modsecurity.d/modsecurity-override.conf /etc/nginx/templates/modsec COPY src/etc/modsecurity.d/setup.conf /etc/nginx/templates/modsecurity.d/setup.conf.template COPY nginx/docker-entrypoint.d/*.sh /docker-entrypoint.d/ COPY src/opt/modsecurity/activate-plugins.sh /docker-entrypoint.d/94-activate-plugins.sh -COPY src/opt/modsecurity/activate-rules.sh /docker-entrypoint.d/95-activate-rules.sh +COPY src/opt/modsecurity/configure-rules.sh /docker-entrypoint.d/95-configure-rules.sh +COPY src/opt/modsecurity/configure-rules.conf /docker-entrypoint.d/ # We use the templating mechanism from the nginx image here. COPY nginx/templates /etc/nginx/templates/ COPY src/bin/* /usr/local/bin/ @@ -221,6 +221,7 @@ RUN set -eux; \ LD_LIBRARY_PATH="" apt-get install -y -qq --no-install-recommends --no-install-suggests \ ca-certificates \ curl \ + ed \ libcurl4-gnutls-dev \ libfuzzy2 \ liblua${LUA_VERSION} \ diff --git a/nginx/Dockerfile-alpine b/nginx/Dockerfile-alpine index 0d3a3a8..069eded 100644 --- a/nginx/Dockerfile-alpine +++ b/nginx/Dockerfile-alpine @@ -200,7 +200,8 @@ COPY src/etc/modsecurity.d/modsecurity-override.conf /etc/nginx/templates/modsec COPY src/etc/modsecurity.d/setup.conf /etc/nginx/templates/modsecurity.d/setup.conf.template COPY nginx/docker-entrypoint.d/*.sh /docker-entrypoint.d/ COPY src/opt/modsecurity/activate-plugins.sh /docker-entrypoint.d/94-activate-plugins.sh -COPY src/opt/modsecurity/activate-rules.sh /docker-entrypoint.d/95-activate-rules.sh +COPY src/opt/modsecurity/configure-rules.sh /docker-entrypoint.d/95-configure-rules.sh +COPY src/opt/modsecurity/configure-rules.conf /docker-entrypoint.d/ # We use the templating mechanism from the nginx image here. COPY nginx/templates /etc/nginx/templates/ COPY src/bin/* /usr/local/bin/ @@ -211,6 +212,7 @@ RUN set -eux; \ apk add --no-cache \ curl \ curl-dev \ + ed \ libfuzzy2 \ libmaxminddb-dev \ libstdc++ \ diff --git a/src/opt/modsecurity/activate-plugins.sh b/src/opt/modsecurity/activate-plugins.sh index 1534817..7c8a83d 100755 --- a/src/opt/modsecurity/activate-plugins.sh +++ b/src/opt/modsecurity/activate-plugins.sh @@ -1,5 +1,7 @@ #!/bin/sh -e +printf "# # #\nRunning CRS plugin activation\n- - -\n\n" + # Check if crs-setup.conf is overriden if [ -n "${MANUAL_MODE}" ]; then echo "Using manual config mode" @@ -25,3 +27,4 @@ for suffix in "config" "before" "after"; do fi done +printf -- "- - -\nFinished CRS plugin activation\n# # #\n\n" diff --git a/src/opt/modsecurity/activate-rules.sh b/src/opt/modsecurity/activate-rules.sh deleted file mode 100755 index a88e150..0000000 --- a/src/opt/modsecurity/activate-rules.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/bin/sh -e - -setup_conf_path="/etc/modsecurity.d/owasp-crs/crs-setup.conf" - -# Check if crs-setup.conf is overriden -if [ -n "${MANUAL_MODE}" ]; then - echo "Using manual config mode" - return; # Don't use exit on a sourced script -fi - -# Paranoia Level -sed -z -E -i 's/#SecAction[^"]+"id:900000.*tx\.paranoia_level=1\"/SecAction \\\n \"id:900000, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.paranoia_level='"${PARANOIA}"'\"/' "${setup_conf_path}" - -# Blocking Paranoia Level -if [ -n "${BLOCKING_PARANOIA}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900000.*tx\.blocking_paranoia_level=1\"/SecAction \\\n \"id:900000, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.blocking_paranoia_level='"${BLOCKING_PARANOIA}"'\"/' "${setup_conf_path}" -fi - -# Executing Paranoia Level -if [ -n "${EXECUTING_PARANOIA}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900001.*tx\.executing_paranoia_level=1\"/SecAction \\\n \"id:900001, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.executing_paranoia_level='"${EXECUTING_PARANOIA}"'\"/' "${setup_conf_path}" -fi - -# Detection Paranoia Level -if [ -n "${DETECTION_PARANOIA}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900001.*tx\.detection_paranoia_level=1\"/SecAction \\\n \"id:900001, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.detection_paranoia_level='"${DETECTION_PARANOIA}"'\"/' "${setup_conf_path}" -fi - -# Enforce Body Processor URLENCODED -if [ -n "${ENFORCE_BODYPROC_URLENCODED}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900010.*tx\.enforce_bodyproc_urlencoded=1\"/SecAction \\\n \"id:900010, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.enforce_bodyproc_urlencoded='"${ENFORCE_BODYPROC_URLENCODED}"'\"/' "${setup_conf_path}" -fi - -# Inbound and Outbound Anomaly Score -sed -z -E -i 's/#SecAction[^"]+"id:900110.*tx\.outbound_anomaly_score_threshold=4\"/SecAction \\\n \"id:900110, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.inbound_anomaly_score_threshold='"${ANOMALY_INBOUND}"', \\\n setvar:tx.outbound_anomaly_score_threshold='"${ANOMALY_OUTBOUND}"'\"/' "${setup_conf_path}" - -# HTTP methods that a client is allowed to use. -if [ -n "${ALLOWED_METHODS}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900200.*\x27tx\.allowed_methods=[[:upper:][:space:]]*\x27\"/SecAction \\\n \"id:900200, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:\x27tx.allowed_methods='"${ALLOWED_METHODS}"'\x27\"/' "${setup_conf_path}" -fi - -# Content-Types that a client is allowed to send in a request. -if [ -n "${ALLOWED_REQUEST_CONTENT_TYPE}" ]; then - sed -z -E -i 's;#SecAction[^"]+"id:900220.*\x27tx\.allowed_request_content_type=[[:lower:][:space:]|+/-]*\x27\";SecAction \\\n \"id:900220, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:\x27tx.allowed_request_content_type='"${ALLOWED_REQUEST_CONTENT_TYPE}"'\x27\";' "${setup_conf_path}" -fi - -# Content-Types charsets that a client is allowed to send in a request. -if [ -n "${ALLOWED_REQUEST_CONTENT_TYPE_CHARSET}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900280.*\x27tx\.allowed_request_content_type_charset=[[:lower:][:digit:]|-]*\x27\"/SecAction \\\n \"id:900280, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:\x27tx.allowed_request_content_type_charset='"${ALLOWED_REQUEST_CONTENT_TYPE_CHARSET}"'\x27\"/' "${setup_conf_path}" -fi - -# Allowed HTTP versions. -if [ -n "${ALLOWED_HTTP_VERSIONS}" ]; then - sed -z -E -i 's|#SecAction[^"]+"id:900230.*\x27tx\.allowed_http_versions=[HTP012[:space:]/.]*\x27\"|SecAction \\\n \"id:900230, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:\x27tx.allowed_http_versions='"${ALLOWED_HTTP_VERSIONS}"'\x27\"|' "${setup_conf_path}" -fi - -# Forbidden file extensions. -if [ -n "${RESTRICTED_EXTENSIONS}" ]; then - sed -z -E -i 's|#SecAction[^"]+"id:900240.*\x27tx\.restricted_extensions=[[:lower:][:space:]./]*\/\x27\"|SecAction \\\n \"id:900240, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:\x27tx.restricted_extensions='"${RESTRICTED_EXTENSIONS}"'\x27\"|' "${setup_conf_path}" -fi - -# Forbidden request headers basic. -if [ -n "${RESTRICTED_HEADERS_BASIC}" ]; then - sed -z -E -i 's|#SecAction[^"]+"id:900250.*\x27tx\.restricted_headers_basic=[[:lower:][:space:]/-]*\x27\"|SecAction \\\n \"id:900250, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:\x27tx.restricted_headers_basic='"${RESTRICTED_HEADERS_BASIC}"'\x27\"|' "${setup_conf_path}" -fi - -# Forbidden request headers extended. -if [ -n "${RESTRICTED_HEADERS_EXTENDED}" ]; then - sed -z -E -i 's|#SecAction[^"]+"id:900255.*\x27tx\.restricted_headers_extended=[[:lower:][:space:]/-]*\x27\"|SecAction \\\n \"id:900255, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:\x27tx.restricted_headers_extended='"${RESTRICTED_HEADERS_EXTENDED}"'\x27\"|' "${setup_conf_path}" -fi - -# File extensions considered static files. -if [ -n "${STATIC_EXTENSIONS}" ]; then - sed -z -E -i 's|#SecAction[^"]+"id:900260.*\x27tx\.static_extensions=/[[:lower:][:space:]/.]*\x27\"|SecAction \\\n \"id:900260, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:\x27tx.static_extensions='"${STATIC_EXTENSIONS}"'\x27\"|' "${setup_conf_path}" -fi - -# Block request if number of arguments is too high -if [ -n "${MAX_NUM_ARGS}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900300.*tx\.max_num_args=255\"/SecAction \\\n \"id:900300, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.max_num_args='"${MAX_NUM_ARGS}"'\"/' "${setup_conf_path}" -fi - -# Block request if the length of any argument name is too high -if [ -n "${ARG_NAME_LENGTH}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900310.*tx\.arg_name_length=100\"/SecAction \\\n \"id:900310, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.arg_name_length='"${ARG_NAME_LENGTH}"'\"/' "${setup_conf_path}" -fi - -# Block request if the length of any argument value is too high -if [ -n "${ARG_LENGTH}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900320.*tx\.arg_length=400\"/SecAction \\\n \"id:900320, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.arg_length='"${ARG_LENGTH}"'\"/' "${setup_conf_path}" -fi - -# Block request if the total length of all combined arguments is too high -if [ -n "${TOTAL_ARG_LENGTH}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900330.*tx\.total_arg_length=64000\"/SecAction \\\n \"id:900330, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.total_arg_length='"${TOTAL_ARG_LENGTH}"'\"/' "${setup_conf_path}" -fi - -# Block request if the total length of all combined arguments is too high -if [ -n "${MAX_FILE_SIZE}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900340.*tx\.max_file_size=1048576\"/SecAction \\\n \"id:900340, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.max_file_size='"${MAX_FILE_SIZE}"'\"/' "${setup_conf_path}" -fi - -# Block request if the total size of all combined uploaded files is too high -if [ -n "${COMBINED_FILE_SIZES}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900350.*tx\.combined_file_sizes=1048576\"/SecAction \\\n \"id:900350, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.combined_file_sizes='"${COMBINED_FILE_SIZES}"'\"/' "${setup_conf_path}" -fi - -# Activate UTF8 validation -if [ -n "${VALIDATE_UTF8_ENCODING}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900950.*tx\.crs_validate_utf8_encoding=1\"/SecAction \\\n \"id:900950, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.crs_validate_utf8_encoding=1\"/' "${setup_conf_path}" -fi - -# Add SecDefaultActions -if [ -n "${MODSEC_DEFAULT_PHASE1_ACTION}" ]; then - sed -z -E -i "s/SecDefaultAction \"phase:1,log,auditlog,pass\"/SecDefaultAction \"${MODSEC_DEFAULT_PHASE1_ACTION}\"/" "${setup_conf_path}" -fi - -if [ -n "${MODSEC_DEFAULT_PHASE2_ACTION}" ]; then - sed -z -E -i "s/SecDefaultAction \"phase:2,log,auditlog,pass\"/SecDefaultAction \"${MODSEC_DEFAULT_PHASE2_ACTION}\"/" "${setup_conf_path}" -fi - -# Substitute MODSEC_TAG -if [ -n "${MODSEC_TAG}" ]; then - sed -z -E -i "s/\\$\{MODSEC_TAG\}/${MODSEC_TAG}/g" "${setup_conf_path}" -fi - -# Reporting Level -if [ -n "${REPORTING_LEVEL}" ]; then - sed -z -E -i 's/#SecAction[^"]+"id:900115.*tx\.reporting_level=2\"/SecAction \\\n \"id:900115, \\\n phase:1, \\\n nolog, \\\n pass, \\\n t:none, \\\n setvar:tx.reporting_level='"${REPORTING_LEVEL}"'\"/' "${setup_conf_path}" -fi - - -# Add marker rule for CRS test setup -# Add it only once -if [ -n "${CRS_ENABLE_TEST_MARKER}" ] && [ ${CRS_ENABLE_TEST_MARKER} -eq 1 ] && ! grep -q id:999999 "${setup_conf_path}"; then - cat <> "${setup_conf_path}" - - -# Write the value from the X-CRS-Test header as a marker to the log -SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \\ - "id:999999,\\ - phase:1,\\ - pass,\\ - t:none,\\ - log,\\ - msg:'%{MATCHED_VAR}',\ - ctl:ruleRemoveById=1-999999" -EOF -fi diff --git a/src/opt/modsecurity/configure-rules.conf b/src/opt/modsecurity/configure-rules.conf new file mode 100644 index 0000000..82ed65b --- /dev/null +++ b/src/opt/modsecurity/configure-rules.conf @@ -0,0 +1,28 @@ +# Format: ||| +# The octothorpe (#) designates a comment, comments are ignored + +# Superceded by BLOCKING_PARANOIA +true|PARANOIA|900000|blocking_paranoia_level +true|PARANOIA|900001|detection_paranoia_level +false|BLOCKING_PARANOIA|900000|blocking_paranoia_level +# Superceded by DETECTION_PARANOIA +true|EXECUTING_PARANOIA|900001|executing_paranoia_level +false|DETECTION_PARANOIA|900001|detection_paranoia_level +false|ENFORCE_BODYPROC_URLENCODED|900010|enforce_bodyproc_urlencoded +false|INBOUND_ANOMALY|900110|inbound_anomaly_score_threshold +false|OUTBOUND_ANOMALY|900110|outbound_anomaly_score_threshold +false|ALLOWED_METHODS|900200|allowed_methods +false|ALLOWED_REQUEST_CONTENT_TYPE|900220|allowed_request_content_type +false|ALLOWED_REQUEST_CONTENT_TYPE_CHARSET|900280|allowed_request_content_type_charset +false|ALLOWED_HTTP_VERSIONS|900230|allowed_http_versions +false|RESTRICTED_EXTENSIONS|900240|restricted_extensions +false|RESTRICTED_HEADERS_BASIC|900250|restricted_headers_basic +false|RESTRICTED_HEADERS_EXTENDED|900255|restricted_headers_extended +false|MAX_NUM_ARGS|900300|max_num_args +false|ARG_NAME_LENGTH|900310|arg_name_length +false|ARG_LENGTH|900230|arg_length +false|TOTAL_ARG_LENGTH|900330|total_arg_length +false|MAX_FILE_SIZE|900340|max_file_size +false|COMBINED_FILE_SIZES|900350|combined_file_sizes +false|VALIDATE_UTF8_ENCODING|900950|crs_validate_utf8_encoding +false|REPORTING_LEVEL|900115|reporting_level diff --git a/src/opt/modsecurity/configure-rules.sh b/src/opt/modsecurity/configure-rules.sh new file mode 100755 index 0000000..0254ceb --- /dev/null +++ b/src/opt/modsecurity/configure-rules.sh @@ -0,0 +1,171 @@ +#!/bin/sh + +# This script is compatible with busybox. ShellCheck requires the shebang +# to be `/bin/busybox sh`, but non-busybox shells will obviously choke on +# that. So we use `/bin/sh` to launch the "default" shell. + +set -e + +printf "# # #\nRunning CRS rule configuration\n- - -\n" + +DIRECTORY="$(cd "$(dirname "$0")" && pwd)" + +# Check if crs-setup.conf is overriden +if [ -n "${MANUAL_MODE}" ]; then + echo "Using manual config mode" + # Don't use exit on a sourced script + return +fi + + +setup_conf_path="/etc/modsecurity.d/owasp-crs/crs-setup.conf" + +set_value() { + local rule="${1}" + local var_name="${2}" + local tx_var_name="${3}" + local var_value="${4}" + echo "Configuring ${rule} for ${var_name} with ${tx_var_name}=${var_value}" + + # For each rule, we do one pass to uncomment the rule (up to first blank line after the rule), + # then a second pass to set the variable. We do two separate passes since the rule might + # already be uncommented (by default in the file or due to having been uncommented in a previous step). + if grep -Eq "#.*id:${rule}" "${setup_conf_path}"; then + # commented, uncomment now + ed -s "${setup_conf_path}" < /dev/null +/id:${rule}/ +- +.,/^$/ s/#// +wq +EOF + fi + + # uncommented, set var + ed -s "${setup_conf_path}" < /dev/null +/id:${rule}/ +/setvar:'\?tx\.${tx_var_name}=/ +s/=.*"/=${var_value}"/ +wq +EOF +} + +should_set() { + test -n "${1}" +} + +can_set() { + local rule="${1}" + local tx_var_name="${2}" + + if ! grep -q "id:${rule}" "${setup_conf_path}"; then + return 1 + elif ! grep -Eq "setvar:'?tx\.${tx_var_name}" "${setup_conf_path}"; then + return 1 + else + return 0 + fi +} + +get_legacy() { + echo "${1}" | awk -F'\|' '{print $1}' +} + +get_var_name() { + echo "${1}" | awk -F'\|' '{print $2}' +} + +get_var_value() { + # Get the variable name, produce "${}" and use eval to expand + eval "echo $(echo "${1}" | awk -F'\|' '{print "${"$2"}"}')" +} + +get_rule() { + echo "${1}" | awk -F'\|' '{print $3}' +} + +get_tx_var_name() { + echo "${1}" | awk -F'\|' '{print $4}' +} + +while read -r line; do + if [ -z "${line}" ] || echo "${line}" | grep -Eq "^#"; then + continue + fi + + legacy="$(get_legacy "${line}")" + var_name="$(get_var_name "${line}")" + var_value="$(get_var_value "${line}")" + rule="$(get_rule "${line}")" + tx_var_name="$(get_tx_var_name "${line}")" + + if should_set "${var_value}" "${tx_var_name}"; then + if ! can_set "${rule}" "${tx_var_name}"; then + if [ "${legacy}" = "true" ]; then + echo "Legacy variable set but nothing found to substitute. Skipping" + continue + fi + echo "Failed to find rule ${rule} to set ${tx_var_name}=${var_value} for ${var_name} in ${setup_conf_path}. Aborting" + exit 1 + fi + + set_value "${rule}" "${var_name}" "${tx_var_name}" "${var_value}" + fi +done < "${DIRECTORY}/configure-rules.conf" + +# Add SecDefaultActions +var="${MODSEC_DEFAULT_PHASE1_ACTION}" +if should_set "${var}"; then + if ! grep -Eq "^SecDefaultAction.*phase:1" "${setup_conf_path}"; then + echo "Failed to find definition of SecDefaultAction for phase 1 in ${setup_conf_path}. Aborting" + exit 1 + fi + ed -s "${setup_conf_path}" < /dev/null +/^SecDefaultAction.*phase:1/ +s/".*"/"${var}"/ +wq +EOF +fi +var="${MODSEC_DEFAULT_PHASE2_ACTION}" +if should_set "${var}"; then + if ! grep -Eq "^SecDefaultAction.*phase:2" "${setup_conf_path}"; then + echo "Failed to find definition of SecDefaultAction for phase 2 in ${setup_conf_path}. Aborting" + exit 1 + fi + ed -s "${setup_conf_path}" < /dev/null +/^SecDefaultAction.*phase:2/ +s/".*"/"${var}"/ +wq +EOF +fi + +# Substitute MODSEC_TAG (part of the default phase actions above) +var="${MODSEC_TAG}" +if should_set "${var}"; then + if ! grep -q "MODSEC_TAG" "${setup_conf_path}"; then + echo "Failed to find definition of MODSEC_TAG in ${setup_conf_path}. Skipping" + + else + sed -z -E -i "s/\\$\{MODSEC_TAG\}/${var}/g" "${setup_conf_path}" + fi +fi + + +# Add marker rule for CRS test setup +# Add it only once +if [ -n "${CRS_ENABLE_TEST_MARKER}" ] && [ "${CRS_ENABLE_TEST_MARKER}" -eq 1 ] && ! grep -q id:999999 "${setup_conf_path}"; then + cat <> "${setup_conf_path}" + + +# Write the value from the X-CRS-Test header as a marker to the log +SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \\ + "id:999999,\\ + phase:1,\\ + pass,\\ + t:none,\\ + log,\\ + msg:'%{MATCHED_VAR}',\ + ctl:ruleRemoveById=1-999999" +EOF +fi + +printf -- "- - -\nFinished CRS rule configuration\n# # #\n\n" From 9f08de46d2041dcd1a6865ca9ec2609d1b850cb5 Mon Sep 17 00:00:00 2001 From: Max Leske <250711+theseion@users.noreply.github.com> Date: Tue, 24 Dec 2024 09:28:57 +0100 Subject: [PATCH 3/3] chore: add simple test facility for rule configuration --- .github/workflows/configure-rules-for-test.sh | 24 +++++++++ .github/workflows/verifyimage.yml | 10 +++- src/opt/modsecurity/configure-rules.conf | 49 ++++++++++--------- src/opt/modsecurity/configure-rules.sh | 14 ++++-- 4 files changed, 67 insertions(+), 30 deletions(-) create mode 100755 .github/workflows/configure-rules-for-test.sh diff --git a/.github/workflows/configure-rules-for-test.sh b/.github/workflows/configure-rules-for-test.sh new file mode 100755 index 0000000..6b3afd6 --- /dev/null +++ b/.github/workflows/configure-rules-for-test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# The purpose of this script is to test that `configure-rules.sh` will run +# successfully for all variables that we configure. + +set -e + +conf_file="${1}" +env_file="${2}" + +if [ -f "${env_file}" ]; then + rm "${env_file}" +fi + +while read -r line; do + if [ -z "${line}" ] || echo "${line}" | grep -Eq "^#"; then + continue + fi + + var_name="$(cut -d'|' -f2 <<< "${line}")" + test_value="$(cut -d'|' -f5 <<< "${line}")" + echo "Setting ${var_name}=${test_value}" + echo "${var_name}=${test_value}" >> "${env_file}" +done < "${conf_file}" diff --git a/.github/workflows/verifyimage.yml b/.github/workflows/verifyimage.yml index e0ffa8e..7906d1b 100644 --- a/.github/workflows/verifyimage.yml +++ b/.github/workflows/verifyimage.yml @@ -62,8 +62,16 @@ jobs: - name: Run ${{ matrix.target }} run: | + . .github/workflows/configure-rules-for-test.sh \ + src/opt/modsecurity/configure-rules.conf \ + "$(pwd)/${{ matrix.target }}.env" echo "Starting container ${{ matrix.target }}" - docker run --pull "never" -d --name ${{ matrix.target }}-test "${REPO}:${{ matrix.target }}" + docker run \ + --pull "never" \ + -d \ + --name ${{ matrix.target }}-test \ + --env-file "${{ matrix.target }}.env" \ + "${REPO}:${{ matrix.target }}" sleep 30 docker logs ${{ matrix.target }}-test diff --git a/src/opt/modsecurity/configure-rules.conf b/src/opt/modsecurity/configure-rules.conf index 82ed65b..103f59a 100644 --- a/src/opt/modsecurity/configure-rules.conf +++ b/src/opt/modsecurity/configure-rules.conf @@ -1,28 +1,29 @@ -# Format: ||| +# Format: |||| # The octothorpe (#) designates a comment, comments are ignored +# See `.github/workflows/configure-rules-for-test.sh` for how the test value is used. # Superceded by BLOCKING_PARANOIA -true|PARANOIA|900000|blocking_paranoia_level -true|PARANOIA|900001|detection_paranoia_level -false|BLOCKING_PARANOIA|900000|blocking_paranoia_level +true|PARANOIA|900000|blocking_paranoia_level|4 +true|PARANOIA|900001|detection_paranoia_level|4 +false|BLOCKING_PARANOIA|900000|blocking_paranoia_level|4 # Superceded by DETECTION_PARANOIA -true|EXECUTING_PARANOIA|900001|executing_paranoia_level -false|DETECTION_PARANOIA|900001|detection_paranoia_level -false|ENFORCE_BODYPROC_URLENCODED|900010|enforce_bodyproc_urlencoded -false|INBOUND_ANOMALY|900110|inbound_anomaly_score_threshold -false|OUTBOUND_ANOMALY|900110|outbound_anomaly_score_threshold -false|ALLOWED_METHODS|900200|allowed_methods -false|ALLOWED_REQUEST_CONTENT_TYPE|900220|allowed_request_content_type -false|ALLOWED_REQUEST_CONTENT_TYPE_CHARSET|900280|allowed_request_content_type_charset -false|ALLOWED_HTTP_VERSIONS|900230|allowed_http_versions -false|RESTRICTED_EXTENSIONS|900240|restricted_extensions -false|RESTRICTED_HEADERS_BASIC|900250|restricted_headers_basic -false|RESTRICTED_HEADERS_EXTENDED|900255|restricted_headers_extended -false|MAX_NUM_ARGS|900300|max_num_args -false|ARG_NAME_LENGTH|900310|arg_name_length -false|ARG_LENGTH|900230|arg_length -false|TOTAL_ARG_LENGTH|900330|total_arg_length -false|MAX_FILE_SIZE|900340|max_file_size -false|COMBINED_FILE_SIZES|900350|combined_file_sizes -false|VALIDATE_UTF8_ENCODING|900950|crs_validate_utf8_encoding -false|REPORTING_LEVEL|900115|reporting_level +true|EXECUTING_PARANOIA|900001|executing_paranoia_level|4 +false|DETECTION_PARANOIA|900001|detection_paranoia_level|4 +false|ENFORCE_BODYPROC_URLENCODED|900010|enforce_bodyproc_urlencoded|0 +false|INBOUND_ANOMALY|900110|inbound_anomaly_score_threshold|6 +false|OUTBOUND_ANOMALY|900110|outbound_anomaly_score_threshold|6 +false|ALLOWED_METHODS|900200|allowed_methods|GET OPTIONS +false|ALLOWED_REQUEST_CONTENT_TYPE|900220|allowed_request_content_type|application/json +false|ALLOWED_REQUEST_CONTENT_TYPE_CHARSET|900280|allowed_request_content_type_charset|utf-8 +false|ALLOWED_HTTP_VERSIONS|900230|allowed_http_versions|1.1 +false|RESTRICTED_EXTENSIONS|900240|restricted_extensions|.exe/ +false|RESTRICTED_HEADERS_BASIC|900250|restricted_headers_basic|/if/ +false|RESTRICTED_HEADERS_EXTENDED|900255|restricted_headers_extended|/x-some-header/ +false|MAX_NUM_ARGS|900300|max_num_args|100 +false|ARG_NAME_LENGTH|900310|arg_name_length|200 +false|ARG_LENGTH|900230|arg_length|300 +false|TOTAL_ARG_LENGTH|900330|total_arg_length|400 +false|MAX_FILE_SIZE|900340|max_file_size|500 +false|COMBINED_FILE_SIZES|900350|combined_file_sizes|600 +false|VALIDATE_UTF8_ENCODING|900950|crs_validate_utf8_encoding|0 +false|REPORTING_LEVEL|900115|reporting_level|5 diff --git a/src/opt/modsecurity/configure-rules.sh b/src/opt/modsecurity/configure-rules.sh index 0254ceb..362afab 100755 --- a/src/opt/modsecurity/configure-rules.sh +++ b/src/opt/modsecurity/configure-rules.sh @@ -31,20 +31,24 @@ set_value() { # then a second pass to set the variable. We do two separate passes since the rule might # already be uncommented (by default in the file or due to having been uncommented in a previous step). if grep -Eq "#.*id:${rule}" "${setup_conf_path}"; then - # commented, uncomment now + # Commented, uncomment now ed -s "${setup_conf_path}" < /dev/null /id:${rule}/ - -.,/^$/ s/#// +.,/^#\?$/ s/#// wq EOF fi - # uncommented, set var + # Uncommented, set var + # Some rules set multiple vars, so the variable name will be terminated + # by either `,`, `'`, or `"`, depending on whether it's the last line of the rule + # and whether the expression is enclosed in single quotes. + # Use `#` as pattern delimiter, as `/` is part of some variable values. ed -s "${setup_conf_path}" < /dev/null /id:${rule}/ /setvar:'\?tx\.${tx_var_name}=/ -s/=.*"/=${var_value}"/ +s#=[^,'"]\+#=${var_value}# wq EOF } @@ -101,7 +105,7 @@ while read -r line; do if should_set "${var_value}" "${tx_var_name}"; then if ! can_set "${rule}" "${tx_var_name}"; then if [ "${legacy}" = "true" ]; then - echo "Legacy variable set but nothing found to substitute. Skipping" + echo "Legacy variable ${var_name} (${rule}) set but nothing found to substitute. Skipping" continue fi echo "Failed to find rule ${rule} to set ${tx_var_name}=${var_value} for ${var_name} in ${setup_conf_path}. Aborting"