diff --git a/.gitignore b/.gitignore index 450aad5..47188a7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.lai .libs/* src/.libs/* +t/htdocs/index.html diff --git a/.travis.yml b/.travis.yml index bc68604..80ca023 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,15 +20,16 @@ addons: - apache2-dbg - apache2-dev - apache2-mpm-prefork + - libwww-perl env: - - VER_APACHE=2.2.32 - - VER_APACHE=2.4.25 + - VER_APACHE=2.2.34 + - VER_APACHE=2.4.29 before_script: - curl -L https://cpanmin.us | sudo perl - --sudo App::cpanminus - cd .. - - curl https://www.apache.org/dist/httpd/httpd-${VER_APACHE}.tar.gz > httpd-${VER_APACHE}.tar.gz + - curl https://archive.apache.org/dist/httpd/httpd-${VER_APACHE}.tar.gz > httpd-${VER_APACHE}.tar.gz - tar xvzf httpd-${VER_APACHE}.tar.gz - cd httpd-${VER_APACHE} - ./configure --with-mpm=worker @@ -49,8 +50,15 @@ before_script: - pwd - cd - - cd ModSecurity-apache + - ./autogen.sh + - export PATH="/usr/local/apache2/bin/:$PATH" + - ls -la /usr/local/apache2/bin/ + - ls -la /usr/local/apache2/include/ + - ./configure --with-apxs=/usr/local/apache2/bin/apxs --with-libmodsecurity=/usr/local/modsecurity + - make + - sudo make install - pwd script: - - t/TEST.PL -apxs /usr/local/apache2/bin/apxs + - t/TEST -apxs /usr/local/apache2/bin/apxs diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..f6fa474 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,3 @@ +tahirramzan = Tahir Ramzan +zimmerle = Felipe Zimmerle +csanders-git = Chaim Sanders diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..a52a99d --- /dev/null +++ b/CHANGES @@ -0,0 +1,20 @@ +v3.0.x - YYYY-MMM-DD (To be released) +------------------------------------- + + - Asorted fixies on libmodsecurity autoconf. + [Issue #51 - @airween] + - Fix: avoids crash if UNIQUE_ID is not set + [Issue #46 - @Goron1606, @victorhora, @zimmerle] + - Adds make install to the build system + [Issue #36 - @zimmerle, @ROBERT-MCDOWELL] + - Uses mod_unique if available + [Issue #42 - @zimmerle, @victorhora, @Goron1606] + - Fix duplicate HTTP protocol strings + [Issue #21 - @victorhora] + + +v0.0.9-beta1 - 2017-Dec-22 +-------------------------- + + - First version of ModSecurity-apache connector + diff --git a/LICENSE b/LICENSE index d0f53e8..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2016 Chaim sanders + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile.am b/Makefile.am index 5ad0d06..980cd02 100644 --- a/Makefile.am +++ b/Makefile.am @@ -12,8 +12,24 @@ MAINTAINERCLEANFILES = \ modsec-shared-collections-lock CLEANFILES = \ + t/logs/* \ + t/htdocs/index.html \ + t/conf/extra.conf \ + t/conf/httpd.conf \ + t/conf/apache_test_config.pm \ + t/conf/httpd.conf \ + t/conf/mime.types \ + t/conf/modules.conf \ + src/*.lo \ + src/*.slo \ + src/*.o \ + src/*.so \ + src/.libs/*.so + + +CLEANDIRECTORIES = \ t/logs \ - t/conf + src/.libs all: @@ -21,6 +37,13 @@ all: build/apxs-wrapper test: - cd t/ && ./TEST.PL + cd t/ && ./TEST -clean + cd t/ && ./TEST -configure + cd t/ && ./TEST -httpd_conf conf/httpd.conf -httpd @APACHE@ -apxs @APXS@ + + +install-exec-hook: $(pkglib_LTLIBRARIES) + @APXS@ -i -n mod_security3 ./src/.libs/mod_security3.so + .PHONY: all diff --git a/README.md b/README.md index 1f7db0a..5f604ff 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,26 @@ [![](https://raw.githubusercontent.com/ZenHubIO/support/master/zenhub-badge.png)](https://zenhub.com) -The ModSecurity-apache connector is the connection point between Apache and libmodsecurity (ModSecurity v3). Said another way, this project provides a communication channel between Apache and libmodsecurity. This connector is required to use LibModSecurity with Apache. +The ModSecurity-apache connector is the connection point between Apache and libmodsecurity (ModSecurity v3). Said another way, this project provides a communication channel between Apache and libmodsecurity. This connector is required to use LibModSecurity with Apache. The ModSecurity-apache connector takes the form of an Apache module. The module simply serves as a layer of communication between Apache and ModSecurity. Notice that this project depends on libmodsecurity rather than ModSecurity (version 2.9 or less). +### NOTE: This project is not production ready + +This project should be considered under development and not production ready. The functionality is not complete and so should not be used. With Apache HTTP Server, the recommended version of ModSecurity is v2.9.x. + + ### What is the difference between this project and the old ModSecurity module for Apache? -The old version of ModSecurity was origionally designed for and contained within an Apache module. This current version abstracts out some of the details allowing ModSecurity to more easily support multiple platforms and features outside beyond the scope of what Apache internals currently support. As a result using the new libmodsecurity engine is no longer reliant on the use of Apache and can be used to power multiple different connectors. As a result of this the current version is more flexible, has wider support, and allows for the support of new functionality that was not previously possible. +The old version of ModSecurity was originally designed for and contained within an Apache module. This current version abstracts out some of the details allowing ModSecurity to more easily support multiple platforms and features beyond the scope of what Apache internals currently support. As a result using the new libmodsecurity engine is no longer reliant on the use of Apache and can be used to power multiple different connectors. As a result of this the current version is more flexible, has wider support, and allows for the support of new functionality that was not previously possible. -NOTICE: libmodsecurity is not feature complete and it is not considerable -stable, so use is undertaken at your own risk. # Compilation -Before compile this software make sure that you have libmodsecurity installed. +Before you compile this software make sure that you have libmodsecurity installed. You can download it from the ModSecurity git repository. For information pertaining to the compilation and installation of libmodsecurity please consult the documentation provided along with it. With libmodsecurity installed, you can proceed with the installation of the ModSecurity-apache connector. Run the following commands: @@ -33,6 +36,89 @@ $ make $ sudo make install ``` +# Usage + +libModSecurity for Apache extends your configuration directives. In order to load the module into Apache add the following directive to your Apache configuration: + +``` +LoadModule security3_module modules/mod_security3.so +``` + +The module adds new directives to Apache and some of them are documented below: + +modsecurity +----------- +**syntax:** *modsecurity on | off* + +**context:** *http, server, location* + +**default:** *off* + +Turns on or off the ModSecurity functionality. +Note that this configuration directive is no longer related to the SecRule state. +Instead, it now serves solely as an Apache flag to enable or disable the module. + +modsecurity_rules_file +---------------------- +**syntax:** *modsecurity_rules_file <path to rules file>* + +**context:** *http, server, location* + +**default:** *no* + +Specifies the location of the ModSecurity configuration file, e.g.: + +``` +modsecurity on +modsecurity_rules_file /etc/my_modsecurity_rules.conf; +``` + +modsecurity_rules_remote +------------------------ +**syntax:** *modsecurity_rules_remote <key> <URL to rules>* + +**context:** *http, server, location* + +**default:** *no* + +Specifies from where (on the internet) a modsecurity configuration file will be downloaded. +It also specifies the key that will be used to authenticate to that server: + +``` +modsecurity on +modsecurity_rules_remote my-server-key https://my-own-server/rules/download; +``` + +modsecurity_rules +----------------- +**syntax:** *modsecurity_rules <modsecurity rule>* + +**context:** *http, server, location* + +**default:** *no* + +Allows for the direct inclusion of a ModSecurity rule into the Apache configuration. +The following example is loading rules from a file and injecting specific configurations per directory/alias: + +``` +TODO +``` + +modsecurity_transaction_id +-------------------------- +**syntax:** *modsecurity_transaction_id string* + +**context:** *http, server, location* + +**default:** *no* + +Allows to pass transaction ID from Apache instead of generating it in the library. +This can be useful for tracing purposes, e.g. consider this configuration: + +``` +TODO +``` + # Contributing As an open source project we invite (and encourage) anyone from the community to contribute to our project. This may take the form of: new @@ -41,17 +127,12 @@ are willing to help with. Thank you. ## Providing Patches -We prefer to have your patch within the GtiHub infrastructure to facilitate our +We prefer to have your patch within the GitHub infrastructure to facilitate our review work, and our QA integration. GitHub provides an excellent documentation on how to perform “Pull Requests”. More information available here: https://help.github.com/articles/using-pull-requests/ -Please respect the coding style. Pull requests can include various commits, so provide one fix or one piece of functionality per commit. Please do not change anything outside the scope of your target work (e.g. coding style in a function that you have passed by). For further information about the coding style used in this project, please check: https://www.chromium.org/blink/coding-style - -Please respect the coding style in use. Pull requests can include various commits, so -provide one fix or one functionality per commit. Do not change anything outside -the scope of your target work (e.g. coding style in a function that you have -passed by). +Please respect the coding style in use. Pull requests can include various commits, so provide one fix or one piece of functionality per commit. Please do not change anything outside the scope of your target work (e.g. coding style in a function that you have passed by). For further information about the coding style used in this project, please check: https://www.chromium.org/blink/coding-style ### Don’t know where to start? @@ -68,7 +149,7 @@ You may also take a look at recent bug reports and open issues to get an idea of ### Testing your patch Along with the manual testing, we strongly recommend that you to use the Apache test -utility to make sure that you patch does not adversly affect the behavior or performance of Apache. +utility to make sure that you patch does not adversly affect the behavior or performance of Apache. The Apache testing tools are available on: http://httpd.apache.org/test/ @@ -77,7 +158,7 @@ To use those tests .... If you are facing problems getting your added functionality to pass all the Apache tests, feel free to contact us or the Apache mailing list at: http://httpd.apache.org/lists.html -### Debugging +### Debugging Because the ModSecurity Apache Connector runs as part of Apache, one needs to debug the Apache process. Debugging may require several steps. In general debugging can be enabled by compiling the Apache connector with debugging as follows: ```CFLAGS="-g -O0" ./configure ...normal configure parameters...)``` @@ -105,7 +186,7 @@ security@modsecurity.org to report the issue. Once the problem is fixed we will ## Feature Request -We would love to discuss any ideas that you may have for a new feature. Please keep in mind this is a community driven project so be sure to contact the community via the mailing list to get feedback first. Alternativly, feel free to open GitHub issues requesting for new features. Before opening a new issue, please check if there is an existing feature request for the desired functionalityt. +We would love to discuss any ideas that you may have for a new feature. Please keep in mind this is a community driven project so be sure to contact the community via the mailing list to get feedback first. Alternatively, feel free to open GitHub issues requesting for new features. Before opening a new issue, please check if there is an existing feature request for the desired functionality. ## Packing diff --git a/build/apxs-wrapper.in b/build/apxs-wrapper.in index 9dcf8e5..a6d7214 100644 --- a/build/apxs-wrapper.in +++ b/build/apxs-wrapper.in @@ -12,4 +12,4 @@ for opt in "$@"; do esac done -exec @APXS@ -a -c -I @V3INCLUDE@ -L @V3LIB@ -lmodsecurity src/apache_http_modsecurity.c +exec @APXS@ -Wc,-fPIC -Wc,-O0 -a -c -I @V3INCLUDE@ -L @V3LIB@ -lmodsecurity src/mod_security3.c src/msc_config.c src/msc_filters.c src/msc_utils.c diff --git a/build/ax_prog_apache.m4 b/build/ax_prog_apache.m4 index bb58d68..aad2f2d 100644 --- a/build/ax_prog_apache.m4 +++ b/build/ax_prog_apache.m4 @@ -112,7 +112,10 @@ AC_DEFUN([AX_PROG_APACHE], # If not specified by caller, search in standard places # if test -z "$APACHE" ; then - AC_PATH_PROG(APACHE, httpd, , /usr/local/apache/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin) + AC_PATH_PROG(APACHE, httpd, , /usr/local/apache/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/usr/local/apache2/bin) + fi + if test -z "$APACHE" ; then + AC_PATH_PROG(APACHE, apache2, , /usr/local/apache/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/usr/local/apache2/bin) fi AC_SUBST(APACHE) if test -z "$APACHE" ; then @@ -149,7 +152,12 @@ AC_DEFUN([AX_PROG_APACHE], # # Find out if .so modules are in libexec/module.so or modules/module.so # - HTTP_ROOT=`$APACHE -V | grep HTTPD_ROOT | sed -e 's/.*"\(.*\)"/\1/'` + if test -f /etc/apache2/envvars + then + HTTP_ROOT=`. /etc/apache2/envvars && $APACHE -V | grep HTTPD_ROOT | sed -e 's/.*"\(.*\)"/\1/'` + else + HTTP_ROOT=`$APACHE -V | grep HTTPD_ROOT | sed -e 's/.*"\(.*\)"/\1/'` + fi AC_MSG_CHECKING(apache modules) for dir in libexec modules do diff --git a/build/find_libmodsec.m4 b/build/find_libmodsec.m4 index 2a6c82d..3c2bc9d 100644 --- a/build/find_libmodsec.m4 +++ b/build/find_libmodsec.m4 @@ -7,33 +7,24 @@ AC_ARG_WITH(libmodsecurity, [FILE is the path to libmodsecurity install dir; defaults to "/usr/local/modsecurity/".])], [ if test "$withval" = "yes"; then - V3PATH=/usr/local/modsecurity/ + AC_SUBST(CPPFLAGS, "$CPPFLAGS -I/usr/local/modsecurity/include/ -L/usr/local/modsecurity/lib/") + V3INCLUDE="/usr/local/modsecurity/include/" + V3LIB="/usr/local/modsecurity/lib/" else - V3PATH="$withval" + AC_SUBST(CPPFLAGS, "$CPPFLAGS -I${withval}/include/ -L${withval}/lib/") + V3INCLUDE="${withval}/include/" + V3LIB="${withval}/lib/" fi ]) -if test -z "$V3PATH"; then - for i in /usr/local/modsecurity/ \ - /usr/local/sbin \ - /usr/local/bin \ - /usr/sbin \ - /usr/bin; - do - if test -f "$i/lib/libmodsecurity.so"; then - V3LIB="$i/lib/" - fi - if test -f "$i/include/modsecurity/modsecurity.h"; then - V3INCLUDE="$i/include/" - # TODO: test if V3LIB is set - break - fi - done -fi -if test -n "$V3LIB" -a "$V3LIB" != "no" -a -x "$V3LIB" ; then - AC_MSG_NOTICE(found libmodsecurity at $V3LIB) -else - AC_MSG_ERROR(couldn't find libmodsecurity) -fi +dnl Check the ModSecurity libraries (modsecurity) + +AC_CHECK_LIB([modsecurity], [msc_init], [ + AC_DEFINE([HAVE_MODSECURITYLIB], [1], + [Define to 1 if you have the `libmodsecurity' library (-lmodsecurity).])], [ + AC_MSG_ERROR([ModSecurity libraries not found!])]) + +AC_CHECK_HEADERS([modsecurity/modsecurity.h], [], [ + AC_MSG_ERROR([ModSecurity headers not found...])]) ]) diff --git a/configure.ac b/configure.ac index d5b1de3..bcb6096 100644 --- a/configure.ac +++ b/configure.ac @@ -9,12 +9,79 @@ AM_INIT_AUTOMAKE([-Wall -Werror foreign]) FIND_APXS() FIND_LIBMOD() AX_PROG_APACHE() +AC_PATH_PROGS(PERL, [perl perl5], ) AC_SUBST(APXS) AC_SUBST(V3LIB) AC_SUBST(V3INCLUDE) +AC_SUBST(APACHE) + +# Some directories +MSC_BASE_DIR=`pwd` +MSC_PKGBASE_DIR="$MSC_BASE_DIR/.." +MSC_TEST_DIR="$MSC_BASE_DIR/tests" +MSC_REGRESSION_DIR="$MSC_TEST_DIR/regression" +MSC_REGRESSION_SERVERROOT_DIR="$MSC_REGRESSION_DIR/server_root" +MSC_REGRESSION_CONF_DIR="$MSC_REGRESSION_SERVERROOT_DIR/conf" +MSC_REGRESSION_LOGS_DIR="$MSC_REGRESSION_SERVERROOT_DIR/logs" +MSC_REGRESSION_DOCROOT_DIR="$MSC_REGRESSION_SERVERROOT_DIR/htdocs" + +if test -f /etc/apache2/envvars +then + SERVER_MPM=`. /etc/apache2/envvars && $APACHE -V | grep Server\ MPM | awk '{print $3}'` + SERVER_MPM_MODE=`. /etc/apache2/envvars && $APACHE -M | grep mpm_${SERVER_MPM}_module | awk '{print $2}' | sed -e 's/@<:@\@{:@\@:}@@:>@//g'` + SERVER_AUTHZ_MODE=`. /etc/apache2/envvars && $APACHE -M | grep authz_core_module | awk '{print $2}' | sed -e 's/@<:@\@{:@\@:}@@:>@//g'` +else + SERVER_MPM=`$APACHE -V | grep Server\ MPM | awk '{print $3}'` + SERVER_MPM_MODE=`$APACHE -M | grep mpm_${SERVER_MPM}_module | awk '{print $2}' | sed -e 's/@<:@\@{:@\@:}@@:>@//g'` + SERVER_AUTHZ_MODE=`$APACHE -M | grep authz_core_module | awk '{print $2}' | sed -e 's/@<:@\@{:@\@:}@@:>@//g'` +fi + +AC_SUBST(MSC_BASE_DIR) +AC_SUBST(MSC_PKGBASE_DIR) +AC_SUBST(MSC_TEST_DIR) +AC_SUBST(MSC_REGRESSION_DIR) +AC_SUBST(MSC_REGRESSION_SERVERROOT_DIR) +AC_SUBST(MSC_REGRESSION_CONF_DIR) +AC_SUBST(MSC_REGRESSION_LOGS_DIR) +AC_SUBST(MSC_REGRESSION_DOCROOT_DIR) + +AC_SUBST(SERVER_MPM) +AC_SUBST(SERVER_MPM_MODE) +AC_SUBST(SERVER_AUTHZ_MODE) + +echo "Found Apache with MPM ${SERVER_MPM}, ${SERVER_MPM_MODE}." + +APXS_SBINDIR="`$APXS -q SBINDIR`" +APXS_PROGNAME="`$APXS -q PROGNAME`" + +APXS_HTTPD="$APXS_SBINDIR/$APXS_PROGNAME" +AC_SUBST(APXS_HTTPD) +APXS_LIBEXECDIR="`$APXS -q LIBEXECDIR`" +if test "xx$APXS_LIBEXECDIR" = "xx"; then APXS_LIBEXECDIR="`$APXS -q LIBDIR`/modules"; fi +AC_SUBST(APXS_LIBEXECDIR) + +# generating apache depends loadable modules +# authz_core required, if not static +# one mpm required, if not static +echo "" > t/conf/modules.conf + +if @<:@ ${SERVER_AUTHZ_MODE} == "shared" @:>@; then + echo "LoadModule authz_core_module ${APXS_LIBEXECDIR}/mod_authz_core.so" >> t/conf/modules.conf +fi +if @<:@ ${SERVER_MPM_MODE} == "shared" @:>@; then + echo "LoadModule mpm_${SERVER_MPM}_module ${APXS_LIBEXECDIR}/mod_mpm_${SERVER_MPM}.so" >> t/conf/modules.conf +fi +echo "" >> t/conf/modules.conf + + AC_CONFIG_FILES([\ Makefile \ build/apxs-wrapper \ + tests/regression/server_root/conf/httpd.conf \ + tests/regression/misc/40-secRemoteRules.t \ + tests/regression/misc/60-pmfromfile-external.t \ + tests/regression/misc/50-ipmatchfromfile-external.t \ + tests/run-regression-tests.pl ]) #[chmod +x build/apxs-wrapper]) diff --git a/others/basic_rules.conf b/others/basic_rules.conf deleted file mode 100644 index 7d1b6da..0000000 --- a/others/basic_rules.conf +++ /dev/null @@ -1,14 +0,0 @@ -SecDebugLog /dev/stdout -SecDebugLogLevel 9 - - -SecRule REQUEST_HEADERS:User-Agent ".*" "id:1,phase:1,t:sha1,t:hexEncode,setvar:tx.ua_hash=%{MATCHED_VAR}" - -SecAction "phase:2,initcol:ip=%{REMOTE_ADDR}_%{tx.ua_hash}" - -SecRule REQUEST_HEADERS:User-Agent ".*" "id:2,phase:2,setvar:ip.auth_attempt=+1" - -SecRule ARGS:foo "herewego" "id:3,phase:2,setvar:ip.foo=bar" -SecRule IP "bar" "id:4,phase:2" -SecRule IP:auth_attempt "bar" "id:5,phase:2" - diff --git a/others/modsecurity.conf b/others/modsecurity.conf deleted file mode 100644 index 09b7757..0000000 --- a/others/modsecurity.conf +++ /dev/null @@ -1,226 +0,0 @@ -# -- Rule engine initialization ---------------------------------------------- - -# Enable ModSecurity, attaching it to every transaction. Use detection -# only to start with, because that minimises the chances of post-installation -# disruption. -# -SecRuleEngine On - - -# -- Request body handling --------------------------------------------------- - -# Allow ModSecurity to access request bodies. If you don't, ModSecurity -# won't be able to see any POST parameters, which opens a large security -# hole for attackers to exploit. -# -SecRequestBodyAccess On - - -# Enable XML request body parser. -# Initiate XML Processor in case of xml content-type -# -SecRule REQUEST_HEADERS:Content-Type "text/xml" \ - "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" - -# Enable JSON request body parser. -# Initiate JSON Processor in case of JSON content-type; change accordingly -# if your application does not use 'application/json' -# -SecRule REQUEST_HEADERS:Content-Type "application/json" \ - "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" - -# Maximum request body size we will accept for buffering. If you support -# file uploads then the value given on the first line has to be as large -# as the largest file you are willing to accept. The second value refers -# to the size of data, with files excluded. You want to keep that value as -# low as practical. -# -SecRequestBodyLimit 13107200 -SecRequestBodyNoFilesLimit 131072 - -# Store up to 128 KB of request body data in memory. When the multipart -# parser reachers this limit, it will start using your hard disk for -# storage. That is slow, but unavoidable. -# -SecRequestBodyInMemoryLimit 131072 - -# What do do if the request body size is above our configured limit. -# Keep in mind that this setting will automatically be set to ProcessPartial -# when SecRuleEngine is set to DetectionOnly mode in order to minimize -# disruptions when initially deploying ModSecurity. -# -SecRequestBodyLimitAction Reject - -# Verify that we've correctly processed the request body. -# As a rule of thumb, when failing to process a request body -# you should reject the request (when deployed in blocking mode) -# or log a high-severity alert (when deployed in detection-only mode). -# -SecRule REQBODY_ERROR "!@eq 0" \ -"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - -# By default be strict with what we accept in the multipart/form-data -# request body. If the rule below proves to be too strict for your -# environment consider changing it to detection-only. You are encouraged -# _not_ to remove it altogether. -# -SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ -"id:'200003',phase:2,t:none,log,deny,status:400, \ -msg:'Multipart request body failed strict validation: \ -PE %{REQBODY_PROCESSOR_ERROR}, \ -BQ %{MULTIPART_BOUNDARY_QUOTED}, \ -BW %{MULTIPART_BOUNDARY_WHITESPACE}, \ -DB %{MULTIPART_DATA_BEFORE}, \ -DA %{MULTIPART_DATA_AFTER}, \ -HF %{MULTIPART_HEADER_FOLDING}, \ -LF %{MULTIPART_LF_LINE}, \ -SM %{MULTIPART_MISSING_SEMICOLON}, \ -IQ %{MULTIPART_INVALID_QUOTING}, \ -IP %{MULTIPART_INVALID_PART}, \ -IH %{MULTIPART_INVALID_HEADER_FOLDING}, \ -FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" - -# Did we see anything that might be a boundary? -# -SecRule MULTIPART_UNMATCHED_BOUNDARY "!@eq 0" \ -"id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" - -# PCRE Tuning -# We want to avoid a potential RegEx DoS condition -# -SecPcreMatchLimit 1000 -SecPcreMatchLimitRecursion 1000 - -# Some internal errors will set flags in TX and we will need to look for these. -# All of these are prefixed with "MSC_". The following flags currently exist: -# -# MSC_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded. -# -SecRule TX:/^MSC_/ "!@streq 0" \ - "id:'200005',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'" - - -# -- Response body handling -------------------------------------------------- - -# Allow ModSecurity to access response bodies. -# You should have this directive enabled in order to identify errors -# and data leakage issues. -# -# Do keep in mind that enabling this directive does increases both -# memory consumption and response latency. -# -SecResponseBodyAccess On - -# Which response MIME types do you want to inspect? You should adjust the -# configuration below to catch documents but avoid static files -# (e.g., images and archives). -# -SecResponseBodyMimeType text/plain text/html text/xml - -# Buffer response bodies of up to 512 KB in length. -SecResponseBodyLimit 524288 - -# What happens when we encounter a response body larger than the configured -# limit? By default, we process what we have and let the rest through. -# That's somewhat less secure, but does not break any legitimate pages. -# -SecResponseBodyLimitAction ProcessPartial - - -# -- Filesystem configuration ------------------------------------------------ - -# The location where ModSecurity stores temporary files (for example, when -# it needs to handle a file upload that is larger than the configured limit). -# -# This default setting is chosen due to all systems have /tmp available however, -# this is less than ideal. It is recommended that you specify a location that's private. -# -SecTmpDir /tmp/ - -# The location where ModSecurity will keep its persistent data. This default setting -# is chosen due to all systems have /tmp available however, it -# too should be updated to a place that other users can't access. -# -SecDataDir /tmp/ - - -# -- File uploads handling configuration ------------------------------------- - -# The location where ModSecurity stores intercepted uploaded files. This -# location must be private to ModSecurity. You don't want other users on -# the server to access the files, do you? -# -#SecUploadDir /opt/modsecurity/var/upload/ - -# By default, only keep the files that were determined to be unusual -# in some way (by an external inspection script). For this to work you -# will also need at least one file inspection rule. -# -#SecUploadKeepFiles RelevantOnly - -# Uploaded files are by default created with permissions that do not allow -# any other user to access them. You may need to relax that if you want to -# interface ModSecurity to an external program (e.g., an anti-virus). -# -#SecUploadFileMode 0600 - - -# -- Debug log configuration ------------------------------------------------- - -# The default debug log configuration is to duplicate the error, warning -# and notice messages from the error log. -# -SecDebugLog /opt/ModSecurity/debug.log -SecDebugLogLevel 9 - - -# -- Audit log configuration ------------------------------------------------- - -# Log the transactions that are marked by a rule, as well as those that -# trigger a server error (determined by a 5xx or 4xx, excluding 404, -# level response status codes). -# -SecAuditEngine On -SecAuditLogRelevantStatus "^(?:5|4(?!04))" - -# Log everything we know about a transaction. -SecAuditLogParts ABIJDEFHZ - -# Use a single file for logging. This is much easier to look at, but -# assumes that you will use the audit log only ocassionally. -# -SecAuditLogType Serial -SecAuditLog /var/log/apache2/modsec_audit.log - -# Specify the path for concurrent audit logging. -#SecAuditLogStorageDir /opt/modsecurity/var/audit/ - - -# -- Miscellaneous ----------------------------------------------------------- - -# Use the most commonly used application/x-www-form-urlencoded parameter -# separator. There's probably only one application somewhere that uses -# something else so don't expect to change this value. -# -SecArgumentSeparator & - -# Settle on version 0 (zero) cookies, as that is what most applications -# use. Using an incorrect cookie version may open your installation to -# evasion attacks (against the rules that examine named cookies). -# -SecCookieFormat 0 - -# Specify your Unicode Code Point. -# This mapping is used by the t:urlDecodeUni transformation function -# to properly map encoded data to your language. Properly setting -# these directives helps to reduce false positives and negatives. -# -SecUnicodeMapFile unicode.mapping 20127 - -# Improve the quality of ModSecurity by sharing information about your -# current ModSecurity version and dependencies versions. -# The following information will be shared: ModSecurity version, -# Web Server version, APR version, PCRE version, Lua version, Libxml2 -# version, Anonymous unique id for host. -SecStatusEngine On - diff --git a/others/security3.conf b/others/security3.conf deleted file mode 100644 index de36e51..0000000 --- a/others/security3.conf +++ /dev/null @@ -1,26 +0,0 @@ - - -sethandler security3_module - -#The argument must be either 'On' or 'Off -modsecurity On - -#Load ModSecurity rules from a path -modsecurity_rules_path /opt/ModSecurity/examples/multiprocess_c/basic_rules.conf - -#Load ModSecurity rules from a file -modsecurity_rules_file path - -#Load ModSecurity rules from a remote server -modsecurity_rules_remote key uri - -#Please ensure that the arugment is specified correctly, including line continuations -modsecurity_rules rule - -#Enable Input Data -IN On - -#Enable Output Data -OUT On - - diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..beaf9f5 --- /dev/null +++ b/release.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +git clean -xfdi +git submodule foreach --recursive git clean -xfdi + +VERSION=`git describe --tags` +DIR_NAME="modsecurity-apache-$VERSION" +TAR_NAME="modsecurity-apache-$VERSION.tar.gz" + +MY_DIR=${PWD##*/} + +cd .. +tar --transform "s/^$MY_DIR/$DIR_NAME/" -cvzf $TAR_NAME --exclude .git $MY_DIR + +sha256sum $TAR_NAME > $TAR_NAME.sha256 +gpg --detach-sign -a $TAR_NAME + +cd - +echo $TAR_NAME ": done." + diff --git a/src/apache_http_modsecurity.c b/src/apache_http_modsecurity.c deleted file mode 100755 index 962822f..0000000 --- a/src/apache_http_modsecurity.c +++ /dev/null @@ -1,381 +0,0 @@ -#include "apache_http_modsecurity.h" - -extern const command_rec module_directives[]; - -const char *apache_http_modsecurity_set_remote_server(cmd_parms *cmd, - void *cfg, - const char *p1, - const char *p2) -{ - apache_http_modsecurity_loc_conf_t *cf = (apache_http_modsecurity_loc_conf_t *) cfg; - if (cf == NULL) - { - return "ModSecurity's remote_server processing directive didn't get an instance."; - } - // Add checks here for p1 and p2 spec - cf->rules_remote_key = p1; - cf->rules_remote_server = p2; - fprintf(stderr, "ModSecurity: License Key: %s, URI: %s\n", p1, p2); - return NULL; -} - - -const char *apache_http_modsecurity_set_file_path(cmd_parms *cmd, - void *cfg, - const char *p) -{ - apache_http_modsecurity_loc_conf_t *cf = (apache_http_modsecurity_loc_conf_t *) cfg; - if (cf == NULL) - { - return "ModSecurity's remote_server processing directive didn't get an instance."; - } - - cf->rules_set = msc_create_rules_set(); - cf->rules_file = NULL; - cf->rules_remote_server = NULL; - cf->rules_remote_key = NULL; - cf->enable = 1; - cf->id = 0; - fprintf(stderr, "ModSecurity creating a location configurationn\n"); - char uri[100] ; - strcpy(uri,p); - const char *err = NULL; - int ret = msc_rules_add_file(cf->rules_set, uri, &err); - fprintf(stderr, "Total Rules '%d' \n",ret); - msc_rules_dump(cf->rules_set); - - return NULL; -} - - -static void *apache_http_modsecurity_merge_loc_conf(apr_pool_t *pool, - void *parent, - void *child) -{ - fprintf(stderr, "Merge Request was called\n\n"); - apache_http_modsecurity_loc_conf_t *p = NULL; - apache_http_modsecurity_loc_conf_t *c = NULL; - apache_http_modsecurity_loc_conf_t *conf = apr_palloc(pool, - sizeof(apache_http_modsecurity_loc_conf_t)); - - p = parent; - c = child; - conf = p; - fprintf(stderr, "Rules set: '%p'\n", conf->rules_set); - if (p->rules_set != NULL) - { - const char *error = NULL; - fprintf(stderr, "We have parental data"); - fprintf(stderr, "Parent is not null, so we have to merge this configurations"); - msc_rules_merge(c->rules_set, p->rules_set, &error); - } - - if (c->rules_remote_server != NULL) - { - int res; - const char *error = NULL; - res = msc_rules_add_remote(c->rules_set, c->rules_remote_key, c->rules_remote_server, &error); - fprintf(stderr, "Loading rules from: '%s'", c->rules_remote_server); - if (res < 0) - { - fprintf(stderr, "Failed to load the rules from: '%s' - reason: '%s'", c->rules_remote_server, error); - - return strdup(error); - } - fprintf(stderr, "Loaded '%d' rules.", res); - } - - if (c->rules_file != NULL) - { - int res; - const char *error = NULL; - res = msc_rules_add_file(c->rules_set, c->rules_file, &error); - fprintf(stderr, "Loading rules from: '%s'", c->rules_file); - if (res < 0) - { - fprintf(stderr, "Failed to load the rules from: '%s' - reason: '%s'", c->rules_file, error); - return strdup(error); - } - fprintf(stderr, "Loaded '%d' rules.", res); - } - - if (c->rules != NULL) - { - int res; - const char *error = NULL; - res = msc_rules_add(c->rules_set, c->rules, &error); - fprintf(stderr, "Loading rules: '%s'", c->rules); - if (res < 0) - { - fprintf(stderr, "Failed to load the rules: '%s' - reason: '%s'", c->rules, error); - return strdup(error); - } - } - msc_rules_dump(c->rules_set); - return conf; -} - -void *apache_http_modsecurity_create_main_conf(apr_pool_t *pool, server_rec *svr) -{ - - apache_http_modsecurity_main_conf_t *md; - md = (apache_http_modsecurity_main_conf_t *) apr_palloc(pool, - sizeof(apache_http_modsecurity_main_conf_t)); - if (md == NULL) - { - fprintf(stderr, "Couldn't allocate space for our main specific configurations"); - return NULL; - } - - md->modsec = NULL; - md->transaction = NULL; - md->modsec = msc_init(); - const char *str1 = "APACHE"; - - msc_set_connector_info(md->modsec, str1); - const char *str2 = msc_who_am_i(md->modsec); - fprintf(stderr, "WMI '%s' \n", str2 ); - - return md; - -} - -void *apache_http_modsecurity_create_loc_conf(apr_pool_t *mp, char *path) -{ - - apache_http_modsecurity_loc_conf_t *cf; - cf = (apache_http_modsecurity_loc_conf_t *) apr_palloc(mp, sizeof(apache_http_modsecurity_loc_conf_t)); - if (cf == NULL) - { - fprintf(stderr, "Couldn't allocate space for our location specific configurations"); - return NULL; - } - - /* cf->rules_set = msc_create_rules_set(); - cf->rules_file = NULL; - cf->rules_remote_server = NULL; - cf->rules_remote_key = NULL; - cf->enable = 1; - cf->id = 0; - fprintf(stderr, "ModSecurity creating a location configurationn\n"); - char uri[] = "/opt/ModSecurity/examples/multiprocess_c/basic_rules.conf"; - const char *err = NULL; - int ret = msc_rules_add_file(cf->rules_set, uri, &err); - fprintf(stderr, "Total Rules '%d' \n",ret); - msc_rules_dump(cf->rules_set);*/ - - return cf; -} - -static void register_hooks(apr_pool_t *pool) -{ - ap_hook_handler(modsec_handler, NULL, NULL, APR_HOOK_REALLY_FIRST); - ap_hook_insert_filter(OutputFilter, NULL, NULL, APR_HOOK_REALLY_FIRST); - ap_register_output_filter("OUT", output_filter, NULL, AP_FTYPE_RESOURCE); - ap_hook_insert_filter(InputFilter, NULL, NULL, APR_HOOK_REALLY_FIRST); - ap_register_input_filter("IN", input_filter, NULL, AP_FTYPE_RESOURCE); -} - -static void OutputFilter(request_rec *r) -{ - FilterConfig *pConfig = ap_get_module_config(r->server->module_config, - &security3_module); - - if (!pConfig->oEnabled) - { - return; - } - - ap_add_output_filter("OUT", NULL, r, r->connection); -} - -static void InputFilter(request_rec *r) -{ - FilterConfig *pConfig = ap_get_module_config(r->server->module_config, - &security3_module); - if (!pConfig->iEnabled) - { - return; - } - - ap_add_input_filter("IN", NULL, r, r->connection); -} - -static int modsec_handler(request_rec *r) -{ - - - if (!r->handler || strcmp(r->handler, "security3_module")) - { - return (DECLINED); - } - - ap_rputs("Welcome to ModSec!
", r); - fprintf(stderr, "Welcome to ModSec!\n"); - return OK; -} - - - -static void *FilterOutCreateServerConfig(apr_pool_t *p, server_rec *s) -{ - FilterConfig *pConfig = apr_pcalloc(p,sizeof *pConfig); - - pConfig->oEnabled = 1; - - return pConfig; -} - -static void *FilterInCreateServerConfig(apr_pool_t *p, server_rec *s) -{ - FilterConfig *pConfig = apr_pcalloc(p, sizeof *pConfig); - - pConfig->iEnabled = 1; - - return pConfig; -} - -static int input_filter(ap_filter_t *f, apr_bucket_brigade *pbbOut, - ap_input_mode_t eMode, apr_read_type_e eBlock, apr_off_t nBytes) -{ - - request_rec *r = f->r; - conn_rec *c = r->connection; - FilterContext *pCtx; - apr_status_t ret; - - apache_http_modsecurity_main_conf_t *md = ap_get_module_config(r->server->module_config, - &security3_module); - apache_http_modsecurity_loc_conf_t *cf = ap_get_module_config(r->server->module_config, - &security3_module); - - md->transaction = msc_new_transaction(md->modsec, cf->rules_set, NULL); - - if (!(pCtx = f->ctx)) - { - f->ctx = pCtx = apr_palloc(r->pool, sizeof *pCtx); - pCtx->pbbTmp = apr_brigade_create(r->pool, c->bucket_alloc); - } - - if (APR_BRIGADE_EMPTY(pCtx->pbbTmp)) - { - ret = ap_get_brigade(f->next, pCtx->pbbTmp, eMode, eBlock, nBytes); - - if (eMode == AP_MODE_EATCRLF || ret != APR_SUCCESS) - return ret; - } - - while (!APR_BRIGADE_EMPTY(pCtx->pbbTmp)) - { - apr_bucket *pbktIn = APR_BRIGADE_FIRST(pCtx->pbbTmp); - apr_bucket *pbktOut; - const char *data; - apr_size_t len; - unsigned char *buf; - apr_size_t n; - - if (APR_BUCKET_IS_EOS(pbktIn)) - { - APR_BUCKET_REMOVE(pbktIn); - APR_BRIGADE_INSERT_TAIL(pbbOut, pbktIn); - break; - } - - ret=apr_bucket_read(pbktIn, &data, &len, eBlock); - if (ret != APR_SUCCESS) - { - return ret; - } - - buf = ap_malloc(len); - for (n=0 ; n < len ; ++n) - { - buf[n] = data[n]; - } - - msc_append_request_body(md->transaction, buf, len); - fprintf(stderr, "req app\n"); - - - pbktOut = apr_bucket_heap_create(buf, len, 0, c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(pbbOut, pbktOut); - apr_bucket_delete(pbktIn); - } - msc_process_request_body(md->transaction); - msc_process_logging(md->transaction); - fprintf(stderr, "req \n"); - - - return APR_SUCCESS; -} - -static int output_filter(ap_filter_t *f, apr_bucket_brigade *pbbIn) -{ - - request_rec *r = f->r; - conn_rec *c = r->connection; - apr_bucket *pbktIn; - apr_bucket_brigade *pbbOut; - - pbbOut = apr_brigade_create(r->pool, c->bucket_alloc); - - - apache_http_modsecurity_main_conf_t *md = ap_get_module_config(r->server->module_config, - &security3_module); - apache_http_modsecurity_loc_conf_t *cf = ap_get_module_config(r->server->module_config, - &security3_module); - - md->transaction = msc_new_transaction(md->modsec, cf->rules_set, NULL); - - for (pbktIn = APR_BRIGADE_FIRST(pbbIn); - pbktIn != APR_BRIGADE_SENTINEL(pbbIn); - pbktIn = APR_BUCKET_NEXT(pbktIn)) - { - const char *data; - apr_size_t len; - unsigned char *buf; - apr_size_t n; - apr_bucket *pbktOut; - - if (APR_BUCKET_IS_EOS(pbktIn)) - { - apr_bucket *pbktEOS = apr_bucket_eos_create(c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(pbbOut, pbktEOS); - continue; - } - - apr_bucket_read(pbktIn, &data, &len, APR_BLOCK_READ); - - buf = apr_bucket_alloc(len, c->bucket_alloc); - for (n=0 ; n < len ; ++n) - { - buf[n] = data[n]; - } - - msc_append_response_body(md->transaction, buf, len); - fprintf(stderr, "res app\n"); - - pbktOut = apr_bucket_heap_create(buf, len, apr_bucket_free, - c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(pbbOut, pbktOut); - } - msc_process_response_body(md->transaction); - msc_process_logging(md->transaction); - fprintf(stderr, "res \n"); - fprintf(stderr, "WMI '%s' \n",msc_who_am_i (md->modsec )); - - - apr_brigade_cleanup(pbbIn); - return ap_pass_brigade(f->next, pbbOut); -} - -module AP_MODULE_DECLARE_DATA security3_module = -{ - STANDARD20_MODULE_STUFF, - apache_http_modsecurity_create_loc_conf, // Per-directory configuration - apache_http_modsecurity_merge_loc_conf, // Merge handler for per-directory - apache_http_modsecurity_create_main_conf, // Per-server conf handler - NULL, // Merge handler for per-server configurations - module_directives, - register_hooks -}; diff --git a/src/apache_http_modsecurity.h b/src/apache_http_modsecurity.h deleted file mode 100644 index ea5261d..0000000 --- a/src/apache_http_modsecurity.h +++ /dev/null @@ -1,74 +0,0 @@ -#include "httpd.h" -#include "http_core.h" -#include "http_config.h" -#include "http_protocol.h" -#include "http_request.h" -#include "http_connection.h" -#include "http_log.h" -#include "ap_expr.h" -#include "apr.h" -#include "apr_hash.h" -#include "apr_strings.h" -#include "apr_buckets.h" -#include "apr_general.h" -#include "apr_lib.h" -#include "util_filter.h" -#define APR_WANT_STRFUNC -#include "apr_want.h" -#include "mod_ssl.h" -#include -#include -#include -#include - -module AP_MODULE_DECLARE_DATA security3_module; - -extern const command_rec module_directives[]; - -static void register_hooks(apr_pool_t *pool); -static int modsec_handler(request_rec *r); -void *apache_http_modsecurity_create_loc_conf(apr_pool_t *mp, char *path); -void *apache_http_modsecurity_create_main_conf(apr_pool_t* pool, server_rec* svr); -static void* apache_http_modsecurity_merge_loc_conf(apr_pool_t* pool, void* parent, void* child); -const char *apache_http_modsecurity_set_remote_server(cmd_parms *cmd, void *cfg, const char *p1, - const char *p2); -const char *apache_http_modsecurity_set_file_path(cmd_parms *cmd, void *cfg, const char *p); - -static void *FilterInCreateServerConfig(apr_pool_t *p, server_rec *s); -static void *FilterOutCreateServerConfig(apr_pool_t *p, server_rec *s); -static const char *FilterInEnable(cmd_parms *cmd, void *dummy, int arg); -static const char *FilterOutEnable(cmd_parms *cmd, void *dummy, int arg); -static int output_filter(ap_filter_t *f, apr_bucket_brigade *pbbIn); -static int input_filter(ap_filter_t *f, apr_bucket_brigade *pbbOut, ap_input_mode_t eMode, - apr_read_type_e eBlock, apr_off_t nBytes); - -static void InputFilter(request_rec *r); -static void OutputFilter(request_rec *r); - -typedef struct -{ - ModSecurity *modsec; - Transaction *transaction; -} apache_http_modsecurity_main_conf_t; - -typedef struct -{ - Rules *rules_set; - char *rules; - char *rules_file; - const char *rules_remote_server; - const char *rules_remote_key; - int enable; - int id; -} apache_http_modsecurity_loc_conf_t; - -typedef struct -{ - int iEnabled; - int oEnabled; -} FilterConfig; - -typedef struct -{ - apr_bucket_brigade *pbbTmp; -} FilterContext; diff --git a/src/apache_http_modsecurity.o b/src/apache_http_modsecurity.o deleted file mode 100644 index c0cbab1..0000000 Binary files a/src/apache_http_modsecurity.o and /dev/null differ diff --git a/src/config.c b/src/config.c deleted file mode 100644 index 5daa68a..0000000 --- a/src/config.c +++ /dev/null @@ -1,65 +0,0 @@ -#include "apache_http_modsecurity.h" - -const command_rec module_directives[] = -{ - AP_INIT_FLAG("modsecurity", - ap_set_flag_slot, - (void *) APR_OFFSETOF(apache_http_modsecurity_loc_conf_t, enable), - OR_OPTIONS, - "The argument must be either 'On' or 'Off'"), - - AP_INIT_TAKE1("modsecurity_rules_file", - ap_set_string_slot, - (void *) APR_OFFSETOF(apache_http_modsecurity_loc_conf_t, - rules_file), - OR_OPTIONS, - "Load ModSecurity rules from a file"), - - AP_INIT_TAKE2("modsecurity_rules_remote", - apache_http_modsecurity_set_remote_server, - NULL, - OR_OPTIONS, - "Load ModSecurity rules from a remote server"), - - AP_INIT_TAKE1("modsecurity_rules_path", - apache_http_modsecurity_set_file_path, - NULL, - OR_OPTIONS, - "Load ModSecurity rules from a path"), - - AP_INIT_TAKE1("modsecurity_rules", - ap_set_string_slot, - (void *) APR_OFFSETOF(apache_http_modsecurity_loc_conf_t, rules), - OR_OPTIONS, - "Please ensure that the arugment is specified correctly, including line continuations."), - - AP_INIT_FLAG("IN", - FilterInEnable, - NULL, - RSRC_CONF, - "Enable Input Data"), - - AP_INIT_FLAG("OUT", - FilterOutEnable, - NULL, - RSRC_CONF, - "Enable Output Data"), - - {NULL} -}; - -static const char *FilterInEnable(cmd_parms *cmd, void *dummy, int arg) -{ - FilterConfig *pConfig = ap_get_module_config(cmd->server->module_config, &security3_module); - pConfig->iEnabled=arg; - - return NULL; -} - -static const char *FilterOutEnable(cmd_parms *cmd, void *dummy, int arg) -{ - FilterConfig *pConfig = ap_get_module_config(cmd->server->module_config, &security3_module); - pConfig->oEnabled=arg; - - return NULL; -} diff --git a/src/io.c b/src/io.c deleted file mode 100644 index 5d4f36c..0000000 --- a/src/io.c +++ /dev/null @@ -1,2 +0,0 @@ -//IO filters functionality goes here -#include "apache_http_modsecurity.h" diff --git a/src/mod_security3.c b/src/mod_security3.c new file mode 100644 index 0000000..76b3fb7 --- /dev/null +++ b/src/mod_security3.c @@ -0,0 +1,609 @@ + +#include + +#include "mod_security3.h" +#include "msc_utils.h" +#include "msc_config.h" + +/* + * + */ +msc_global *msc_apache; + + +void modsecurity_log_cb(void *log, const void* data) +{ + const char *msg; + if (log == NULL || data == NULL) { + return; + } + msg = (const char *) data; + request_rec *r = (request_rec *) log; + +#if AP_SERVER_MAJORVERSION_NUMBER > 1 && AP_SERVER_MINORVERSION_NUMBER > 2 + ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, r, + msg, + r->status); + +#else + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, r->server, + msg, + r->status); +#endif + +} + +int process_intervention (Transaction *t, request_rec *r) +{ + ModSecurityIntervention intervention; + intervention.status = N_INTERVENTION_STATUS; + intervention.url = NULL; + intervention.log = NULL; + intervention.disruptive = 0; + + int z = msc_intervention(t, &intervention); + + if (z == 0) + { + return N_INTERVENTION_STATUS; + } + + if (intervention.log == NULL) + { + intervention.log = "(no log message was specified)"; + } + + if (intervention.status == 301 || intervention.status == 302 + ||intervention.status == 303 || intervention.status == 307) + { + if (intervention.url != NULL) + { + apr_table_setn(r->headers_out, "Location", intervention.url); + return HTTP_MOVED_TEMPORARILY; + } + } + + if (intervention.status != N_INTERVENTION_STATUS) + { + return intervention.status; + } + + return N_INTERVENTION_STATUS; +} + + +/* + * Called only once. Used to initialise the ModSecurity + * + */ +int msc_apache_init(apr_pool_t *mp) +{ + msc_apache = apr_pcalloc(mp, sizeof(msc_global)); + if (msc_apache == NULL) + { + goto err_no_mem; + } + + msc_apache->modsec = msc_init(); + + msc_set_connector_info(msc_apache->modsec, MSC_APACHE_CONNECTOR); + + apr_pool_cleanup_register(mp, NULL, msc_module_cleanup, apr_pool_cleanup_null); + + msc_set_log_cb(msc_apache->modsec, modsecurity_log_cb); + + return 0; + +err_no_mem: + return -1; +} + + +/* + * Called only once. Used to cleanup ModSecurity + * + */ +int msc_apache_cleanup() +{ + msc_cleanup(msc_apache->modsec); +} + + +/* + * Used to cleanup the module + * + */ +static apr_status_t msc_module_cleanup(void *data) +{ + msc_apache_cleanup(); + return APR_SUCCESS; +} + + + +/** + * Stores transaction context where it can be found in subsequent + * phases, redirections, or subrequests. + */ +static void store_tx_context(msc_t *msr, request_rec *r) +{ + apr_table_setn(r->notes, NOTE_MSR, (void *)msr); +} + + +static msc_t *create_tx_context(request_rec *r) { + msc_t *msr = NULL; + msc_conf_t *z = NULL; + char *unique_id = NULL; + + z = (msc_conf_t *)ap_get_module_config(r->per_dir_config, + &security3_module); + + msr = (msc_t *)apr_pcalloc(r->pool, sizeof(msc_t)); + if (msr == NULL) { + return NULL; + } + + msr->r = r; + unique_id = getenv("UNIQUE_ID"); + if (unique_id != NULL && strlen(unique_id) > 0) { + msr->t = msc_new_transaction_with_id(msc_apache->modsec, + z->rules_set, unique_id, (void *)r); + } else { + msr->t = msc_new_transaction(msc_apache->modsec, + z->rules_set, (void *)r); + } + + store_tx_context(msr, r); + + return msr; +} + + +/** + * Retrieves a previously stored transaction context by + * looking at the main request, and the previous requests. + */ +static msc_t *retrieve_tx_context(request_rec *r) { + msc_t *msr = NULL; + request_rec *rx = NULL; + + /* Look in the current request first. */ + msr = (msc_t *)apr_table_get(r->notes, NOTE_MSR); + if (msr != NULL) + { + msr->r = r; + return msr; + } + + /* If this is a subrequest then look in the main request. */ + if (r->main != NULL) + { + msr = (msc_t *)apr_table_get(r->main->notes, NOTE_MSR); + if (msr != NULL) + { + msr->r = r; + return msr; + } + } + + /* If the request was redirected then look in the previous requests. */ + rx = r->prev; + while (rx != NULL) + { + msr = (msc_t *)apr_table_get(rx->notes, NOTE_MSR); + if (msr != NULL) + { + msr->r = r; + return msr; + } + rx = rx->prev; + } + + return NULL; +} + + +static int msc_hook_pre_config(apr_pool_t *mp, apr_pool_t *mp_log, + apr_pool_t *mp_temp) +{ + void *data = NULL; + const char *key = "modsecurity-pre-config-init-flag"; + int first_time = 0; + + /* Figure out if we are here for the first time */ + apr_pool_userdata_get(&data, key, mp); + if (data == NULL) + { + apr_pool_userdata_set((const void *) 1, key, + apr_pool_cleanup_null, mp); + first_time = 1; + } + + if (!first_time) + { + return OK; + } + + // Code to run only at the very first call. + int ret = msc_apache_init(mp); + + if (ret == -1) + { + ap_log_error(APLOG_MARK, APLOG_STARTUP, 0, NULL, + "ModSecurity: Failed to initialise."); + return HTTP_INTERNAL_SERVER_ERROR; + } + + return OK; +} + + +static int msc_hook_post_config(apr_pool_t *mp, apr_pool_t *mp_log, + apr_pool_t *mp_temp, server_rec *s) +{ + void *data = NULL; + const char *key = "modsecurity-post-config-init-flag"; + int first_time = 0; + + /* Figure out if we are here for the first time */ + apr_pool_userdata_get(&data, key, s->process->pool); + if (data == NULL) + { + apr_pool_userdata_set((const void *) 1, key, + apr_pool_cleanup_null, s->process->pool); + first_time = 1; + } + + if (!first_time) + { + return OK; + } + + // Code to run only at the very first call. + ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, 0, s, + "ModSecurity: %s configured.", MSC_APACHE_CONNECTOR); + + return OK; +} + + + +static int hook_connection_early(conn_rec *conn) +{ + // At this point there isn't a request_rec attached to the request, + // therefore we can't create the config yet, lets wait till next phase. + + return DECLINED; +} + + +/** + * Initial request processing, executed immediatelly after + * Apache receives the request headers. This function wil create + * a transaction context. + */ +static int hook_request_early(request_rec *r) { + msc_t *msr = NULL; + int rc = DECLINED; +#if AP_SERVER_MAJORVERSION_NUMBER > 1 && AP_SERVER_MINORVERSION_NUMBER < 3 + const char *client_ip = r->connection->remote_ip; + int client_port = r->connection->remote_addr->port; +#else + const char *client_ip = r->connection->client_ip; + int client_port = r->connection->client_addr->port; +#endif + + /* This function needs to run only once per transaction + * (i.e. subrequests and redirects are excluded). + */ + if ((r->main != NULL) || (r->prev != NULL)) { + return DECLINED; + } + + /* Initialise transaction context and + * create the initial configuration. + */ +#ifdef REQUEST_EARLY +#error "Request Early is not ready for v3 yet." + msr = create_tx_context(r); + if (msr == NULL) + { + return DECLINED; + } +#endif + +#ifndef LATE_CONNECTION_PROCESS +#error "Currently in v3 connection can only be processed late." + msc_process_connection(msr->t, client_ip, + client_port, + r->server->server_hostname, + (int) r->server->port); + + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + return it; + } +#endif + +#ifdef REQUEST_EARLY + it = process_request_headers(r, msr); + if (it != N_INTERVENTION_STATUS) + { + return it; + } +#endif + + return rc; +} + +/** + * Invoked as the first hook in the handler chain, this function + * executes the second phase of ModSecurity request processing. + */ +static int hook_request_late(request_rec *r) +{ + msc_t *msr = NULL; + int it; +#if AP_SERVER_MAJORVERSION_NUMBER > 1 && AP_SERVER_MINORVERSION_NUMBER < 3 + const char *client_ip = r->connection->remote_ip; + int client_port = r->connection->remote_addr->port; +#else + const char *client_ip = r->connection->client_ip; + int client_port = r->connection->client_addr->port; +#endif + + /* This function needs to run only once per transaction + * (i.e. subrequests and redirects are excluded). + */ + if ((r->main != NULL) || (r->prev != NULL)) + { + return DECLINED; + } + + /* Find the transaction context and make sure + * we are supposed to proceed. + */ +#ifdef REQUEST_EARLY + msr = retrieve_tx_context(r); +#else + msr = create_tx_context(r); +#endif + if (msr == NULL) + { + /* If we can't find the context that probably means it's + * a subrequest that was not initiated from the outside. + */ + return DECLINED; + } + +#ifdef LATE_CONNECTION_PROCESS + msc_process_connection(msr->t, client_ip, + client_port, + r->server->server_hostname, + (int) r->server->port); + + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + return it; + } +#endif + +#ifndef REQUEST_EARLY + it = process_request_headers(r, msr); + if (it != N_INTERVENTION_STATUS) + { + return it; + } +#endif + + + msc_process_request_body(msr->t); + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + return it; + } + + return DECLINED; +} + + +/** + * Invoked at the end of each transaction. + */ +static int hook_log_transaction(request_rec *r) +{ + const apr_array_header_t *arr = NULL; + request_rec *origr = NULL; + msc_t *msr = NULL; + int it; + + msr = retrieve_tx_context(r); + if (msr == NULL) + { + return DECLINED; + } + + msc_update_status_code(msr->t, r->status); + msc_process_logging(msr->t); + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + return it; + } + + return DECLINED; +} + + +/** + * Invoked right before request processing begins. This is + * when we need to decide if we want to hook into the output + * filter chain. + */ +static void hook_insert_filter(request_rec *r) +{ + msc_t *msr = NULL; + + /* Find the transaction context first. */ + msr = retrieve_tx_context(r); + if (msr == NULL) + { + return; + } + +#if 1 + /* Add the input filter, but only if we need it to run. */ + ap_add_input_filter("MODSECURITY_IN", msr, r, r->connection); +#endif + + /* The output filters only need to be added only once per transaction + * (i.e. subrequests and redirects are excluded). + */ + if ((r->main != NULL) || (r->prev != NULL)) + { + return; + } + + + ap_add_output_filter("MODSECURITY_OUT", msr, r, r->connection); +} + + +static int process_request_headers(request_rec *r, msc_t *msr) { + /* process uri */ + { + int it; + int offset = (r->protocol && strlen(r->protocol) > 5 && r->protocol[0] == 'H') ? 5 : 0; + + msc_process_uri(msr->t, r->unparsed_uri, r->method, r->protocol + offset); + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + return it; + } + } + + /* add request headers */ + { + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i; + int it; + + arr = apr_table_elts(r->headers_in); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) + { + const char *key = te[i].key; + const char *val = te[i].val; + msc_add_request_header(msr->t, key, val); + } + msc_process_request_headers(msr->t); + + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + return it; + } + } + + return N_INTERVENTION_STATUS; +} + + + +static void msc_register_hooks(apr_pool_t *pool) +{ + static const char *const postconfig_beforeme_list[] = { + "mod_unique_id.c", + "mod_ssl.c", + NULL + }; + + static const char *const postconfig_afterme_list[] = { + "mod_fcgid.c", + "mod_cgid.c", + NULL + }; + + static const char *const postread_beforeme_list[] = { + "mod_rpaf.c", + "mod_rpaf-2.0.c", + "mod_extract_forwarded.c", + "mod_extract_forwarded2.c", + "mod_remoteip.c", + "mod_custom_header.c", + "mod_breach_realip.c", + "mod_breach_trans.c", + "mod_unique_id.c", + NULL + }; + + static const char *const postread_afterme_list[] = { + "mod_log_forensic.c", + NULL + }; + + static const char *const transaction_afterme_list[] = { + "mod_log_config.c", + NULL + }; + + static const char *const fixups_beforeme_list[] = { + "mod_env.c", + NULL + }; + + /* Module initialization */ + ap_hook_pre_config(msc_hook_pre_config, NULL, NULL, APR_HOOK_FIRST); + ap_hook_post_config(msc_hook_post_config, postconfig_beforeme_list, + postconfig_afterme_list, APR_HOOK_REALLY_LAST); + + + /* Connection processing hooks - only global configuration. */ + ap_hook_post_read_request(hook_request_early, + postread_beforeme_list, postread_afterme_list, APR_HOOK_REALLY_FIRST); + + /* still, we don't have location configuration yet. */ + ap_hook_process_connection(hook_connection_early, NULL, NULL, APR_HOOK_FIRST); + + ap_hook_fixups(hook_request_late, fixups_beforeme_list, NULL, APR_HOOK_REALLY_FIRST); + + /* Lets add the remaining hooks */ + ap_hook_insert_filter(hook_insert_filter, NULL, NULL, APR_HOOK_FIRST); + + /* Logging */ + /* ap_hook_error_log is called for every error log entry that apache writes. + * may not be necessary in our particular case. Disabling for now. + * + * ap_hook_error_log(hook_error_log, NULL, NULL, APR_HOOK_MIDDLE); + * + */ + ap_hook_log_transaction(hook_log_transaction, NULL, transaction_afterme_list, APR_HOOK_MIDDLE); + + /* request body */ + ap_register_input_filter("MODSECURITY_IN", input_filter, + NULL, AP_FTYPE_CONTENT_SET); + + /* response body */ + ap_register_output_filter("MODSECURITY_OUT", output_filter, + NULL, AP_FTYPE_CONTENT_SET - 3); +} + + + +module AP_MODULE_DECLARE_DATA security3_module = +{ + STANDARD20_MODULE_STUFF, + msc_hook_create_config_directory, // Per-directory configuration. + msc_hook_merge_config_directory, // Merge handler for per-directory. + NULL, // Per-server conf handler. + NULL, // Merge handler for per-server + // configurations. + module_directives, + msc_register_hooks +}; diff --git a/src/mod_security3.h b/src/mod_security3.h new file mode 100644 index 0000000..b1e9b28 --- /dev/null +++ b/src/mod_security3.h @@ -0,0 +1,101 @@ + + +#include + +#include +#if defined(MODSECURITY_CHECK_VERSION) +#if MODSECURITY_VERSION_NUM >= 304010 +#define MSC_USE_RULES_SET 1 +#endif +#endif + +#if defined(MSC_USE_RULES_SET) +#include +#else +#include +#endif +#include + +#include "apr_buckets.h" +#include "apr_general.h" +#include "apr.h" +#include "apr_hash.h" +#include "apr_lib.h" +#include "apr_strings.h" +#define APR_WANT_STRFUNC +#include "apr_want.h" +#include "util_filter.h" + +#include "httpd.h" +#include "http_config.h" +#include "http_connection.h" +#include "http_core.h" +#include "http_log.h" +#include "http_protocol.h" +#include "http_request.h" + +#include "msc_filters.h" + +#ifndef _SRC_APACHE_HTTP_MODSECURITY__ +#define _SRC_APACHE_HTTP_MODSECURITY__ + +#define NOTE_MSR "modsecurity3-tx-context" +#define MSC_APACHE_CONNECTOR "ModSecurity-Apache v0.1.1-beta" +/* #define REQUEST_EARLY */ +#define LATE_CONNECTION_PROCESS + +#define N_INTERVENTION_STATUS 200 + + +typedef struct +{ + request_rec *r; + Transaction *t; +} msc_t; + + +typedef struct +{ + void *rules_set; + int msc_state; + char *name_for_debug; +} msc_conf_t; + +typedef struct +{ + ModSecurity *modsec; +} msc_global; + +extern module AP_MODULE_DECLARE_DATA security3_module; +extern msc_global *msc_apache; +extern const command_rec module_directives[]; + + +int process_intervention (Transaction *t, request_rec *r); + +int msc_apache_init(apr_pool_t *pool); +int msc_apache_cleanup(); +static apr_status_t msc_module_cleanup(void *data); + + +/* + +static int hook_connection_early(conn_rec *conn); + +static int msc_hook_pre_config(apr_pool_t *mp, apr_pool_t *mp_log, + apr_pool_t *mp_temp); +static int msc_hook_post_config(apr_pool_t *mp, apr_pool_t *mp_log, + apr_pool_t *mp_temp, server_rec *s); + +static int hook_request_late(request_rec *r); +static int hook_request_early(request_rec *r); +static int hook_log_transaction(request_rec *r); + +static void hook_insert_filter(request_rec *r); +*/ +/* +*/ + +static int process_request_headers(request_rec *r, msc_t *msr); + +#endif /* _SRC_APACHE_HTTP_MODSECURITY__ */ diff --git a/src/msc_config.c b/src/msc_config.c new file mode 100644 index 0000000..387fae1 --- /dev/null +++ b/src/msc_config.c @@ -0,0 +1,207 @@ + +#include "mod_security3.h" +#include "msc_config.h" +#include "msc_filters.h" + + +const command_rec module_directives[] = +{ + AP_INIT_TAKE1( + "modsecurity", + msc_config_modsec_state, + NULL, + RSRC_CONF | ACCESS_CONF, + "The argument must be either 'On' or 'Off'" + ), + + AP_INIT_TAKE1( + "modsecurity_rules", + msc_config_load_rules, + NULL, + RSRC_CONF | ACCESS_CONF, + "Please ensure that the arugment is specified correctly, including line continuations." + ), + + AP_INIT_TAKE1( + "modsecurity_rules_file", + msc_config_load_rules_file, + NULL, + RSRC_CONF | ACCESS_CONF, + "Load ModSecurity rules from a file" + ), + + AP_INIT_TAKE2( + "modsecurity_rules_remote", + msc_config_load_rules_remote, + NULL, + RSRC_CONF | ACCESS_CONF, + "Load ModSecurity rules from a remote server" + ), + + {NULL} +}; + + +static const char *msc_config_modsec_state(cmd_parms *cmd, void *_cnf, + const char *p1) +{ + msc_conf_t *cnf = (msc_conf_t *) _cnf; + + if (strcasecmp(p1, "On") == 0) + { + cnf->msc_state = 1; + } + else if (strcasecmp(p1, "Off") == 0) + { + cnf->msc_state = 0; + } + else + { + return "ModSecurity state must be either 'On' or 'Off'"; + } + + return NULL; +} + + +static const char *msc_config_load_rules(cmd_parms *cmd, void *_cnf, + const char *p1) +{ + msc_conf_t *cnf = (msc_conf_t *) _cnf; + const char *error = NULL; + int ret; + + ret = msc_rules_add(cnf->rules_set, p1, &error); + + if (ret < 0) + { + return error; + } + + return NULL; +} + + +static const char *msc_config_load_rules_file(cmd_parms *cmd, void *_cnf, + const char *p1) +{ + msc_conf_t *cnf = (msc_conf_t *) _cnf; + const char *error = NULL; + int ret; + + ret = msc_rules_add_file(cnf->rules_set, p1, &error); + + if (ret < 0) + { + return error; + } + + return NULL; +} + + +static const char *msc_config_load_rules_remote(cmd_parms *cmd, void *_cnf, + const char *p1, const char *p2) +{ + msc_conf_t *cnf = (msc_conf_t *) _cnf; + const char *error = NULL; + int ret; + + ret = msc_rules_add_remote(cnf->rules_set, p1, p2, &error); + + if (ret < 0) + { + return error; + } + + return NULL; +} + +void *msc_hook_create_config_directory(apr_pool_t *mp, char *path) +{ + msc_conf_t *cnf = NULL; + + cnf = apr_pcalloc(mp, sizeof(msc_conf_t)); + if (cnf == NULL) + { + goto end; + } +#if 0 + ap_log_perror(APLOG_MARK, APLOG_STARTUP|APLOG_NOERRNO, 0, mp, + "ModSecurity: Created directory config for path: %s [%pp]", path, cnf); +#endif + + cnf->rules_set = msc_create_rules_set(); + if (path != NULL) + { + cnf->name_for_debug = strdup(path); + } +#if 0 + ap_log_perror(APLOG_MARK, APLOG_STARTUP|APLOG_NOERRNO, 0, mp, + "ModSecurity: Config for path: %s is at: %pp", path, cnf); +#endif + +end: + return cnf; +} + + +void *msc_hook_merge_config_directory(apr_pool_t *mp, void *parent, + void *child) +{ + msc_conf_t *cnf_p = parent; + msc_conf_t *cnf_c = child; + msc_conf_t *cnf_new = (msc_conf_t *)msc_hook_create_config_directory(mp, cnf_c->name_for_debug); + + if (cnf_p && cnf_c) + { + const char *error = NULL; + int ret; +#if 0 + ap_log_perror(APLOG_MARK, APLOG_STARTUP|APLOG_NOERRNO, 0, mp, + "ModSecurity: Merge parent %pp [%s] child %pp [%s]" \ + "into: %pp", cnf_p, + cnf_p->name_for_debug, + child, cnf_c->name_for_debug, cnf_new); +#endif + cnf_new->name_for_debug = cnf_c->name_for_debug; + + ret = msc_rules_merge(cnf_new->rules_set, cnf_c->rules_set, &error); + if (ret < 0) + { + ap_log_perror(APLOG_MARK, APLOG_STARTUP|APLOG_NOERRNO, 0, mp, + "ModSecurity: Rule merge failed: %s", error); + return NULL; + } + + ret = msc_rules_merge(cnf_new->rules_set, cnf_p->rules_set, &error); + if (ret < 0) + { + ap_log_perror(APLOG_MARK, APLOG_STARTUP|APLOG_NOERRNO, 0, mp, + "ModSecurity: Rule merge failed: %s", error); + return NULL; + } +#if 0 + ap_log_perror(APLOG_MARK, APLOG_STARTUP|APLOG_NOERRNO, 0, mp, + "ModSecurity: Merge OK"); +#endif + } + else if (cnf_c && !cnf_p) + { +#if 0 + ap_log_perror(APLOG_MARK, APLOG_STARTUP|APLOG_NOERRNO, 0, mp, + "ModSecurity: Merge parent -NULL- [-NULL-] child %pp [%s]", + cnf_c, cnf_c->name_for_debug); +#endif + } + else if (cnf_p && !cnf_c) + { +#if 0 + ap_log_perror(APLOG_MARK, APLOG_STARTUP|APLOG_NOERRNO, 0, mp, + "ModSecurity: Merge parent %pp [%s] child -NULL- [-NULL-]", + cnf_p, cnf_p->name_for_debug); +#endif + } + + return cnf_new; +} diff --git a/src/msc_config.h b/src/msc_config.h new file mode 100644 index 0000000..e3cd8d9 --- /dev/null +++ b/src/msc_config.h @@ -0,0 +1,24 @@ + +#ifndef _SRC_MSC_CONFIG__ +#define _SRC_MSC_CONFIG__ + +static const char *msc_config_modsec_state(cmd_parms *cmd, void *_dcfg, + const char *p1); + +static const char *msc_config_load_rules(cmd_parms *cmd, void *_dcfg, + const char *p1); + +static const char *msc_config_load_rules_file(cmd_parms *cmd, void *_dcfg, + const char *p1); + +static const char *msc_config_load_rules_remote(cmd_parms *cmd, void *_dcfg, + const char *p1, const char *p2); + +void *msc_hook_create_config_directory(apr_pool_t *mp, char *path); + +void *msc_hook_merge_config_directory(apr_pool_t *mp, void *parent, + void *child); + + + +#endif /* _SRC_MSC_CONFIG__ */ diff --git a/src/msc_filters.c b/src/msc_filters.c new file mode 100644 index 0000000..3a18e21 --- /dev/null +++ b/src/msc_filters.c @@ -0,0 +1,150 @@ + +#include "msc_filters.h" +#include "msc_utils.h" + + +apr_status_t input_filter(ap_filter_t *f, apr_bucket_brigade *pbbOut, + ap_input_mode_t mode, apr_read_type_e block, apr_off_t nbytes) +{ + request_rec *r = f->r; + conn_rec *c = r->connection; + + apr_bucket_brigade *pbbTmp; + int ret; + + msc_t *msr = (msc_t *)f->ctx; + + /* Do we have the context? */ + if (msr == NULL) + { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, f->r->server, + "ModSecurity: Internal Error: msr is null in input filter."); + ap_remove_output_filter(f); + return send_error_bucket(msr, f, HTTP_INTERNAL_SERVER_ERROR); + } + + pbbTmp = apr_brigade_create(r->pool, c->bucket_alloc); + if (APR_BRIGADE_EMPTY(pbbTmp)) + { + ret = ap_get_brigade(f->next, pbbTmp, mode, block, nbytes); + + if (mode == AP_MODE_EATCRLF || ret != APR_SUCCESS) + return ret; + } + + while (!APR_BRIGADE_EMPTY(pbbTmp)) + { + apr_bucket *pbktIn = APR_BRIGADE_FIRST(pbbTmp); + apr_bucket *pbktOut; + const char *data; + apr_size_t len; + apr_size_t n; + int it; + + if (APR_BUCKET_IS_EOS(pbktIn)) + { + APR_BUCKET_REMOVE(pbktIn); + APR_BRIGADE_INSERT_TAIL(pbbOut, pbktIn); + break; + } + + ret=apr_bucket_read(pbktIn, &data, &len, block); + if (ret != APR_SUCCESS) + { + return ret; + } + + msc_append_request_body(msr->t, data, len); + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + ap_remove_output_filter(f); + return send_error_bucket(msr, f, it); + } + + // FIXME: Now we should have the body. Is this sane? + msc_process_request_body(msr->t); + + pbktOut = apr_bucket_heap_create(data, len, 0, c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(pbbOut, pbktOut); + apr_bucket_delete(pbktIn); + } + return APR_SUCCESS; +} + + +apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) +{ + request_rec *r = f->r; + msc_t *msr = (msc_t *)f->ctx; + + /* Do we have the context? */ + if (msr == NULL) + { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, f->r->server, + "ModSecurity: Internal Error: msr is null in output filter."); + ap_remove_output_filter(f); + return send_error_bucket(msr, f, HTTP_INTERNAL_SERVER_ERROR); + } + + /* response headers */ + { + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, it; + + arr = apr_table_elts(r->err_headers_out); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) + { + const char *key = te[i].key; + const char *val = te[i].val; + msc_add_response_header(msr->t, key, val); + } + + arr = apr_table_elts(r->headers_out); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) + { + const char *key = te[i].key; + const char *val = te[i].val; + msc_add_response_header(msr->t, key, val); + } + + msc_process_response_headers(msr->t, r->status, "HTTP 1.1"); + + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + ap_remove_output_filter(f); + return send_error_bucket(msr, f, it); + } + } + + /* response body */ + { + apr_bucket *pbktIn; + int it; + + for (pbktIn = APR_BRIGADE_FIRST(bb_in); + pbktIn != APR_BRIGADE_SENTINEL(bb_in); + pbktIn = APR_BUCKET_NEXT(pbktIn)) + { + const char *data; + apr_size_t len; + apr_bucket_read(pbktIn, &data, &len, APR_BLOCK_READ); + msc_append_response_body(msr->t, data, len); + } + msc_process_response_body(msr->t); + + it = process_intervention(msr->t, r); + if (it != N_INTERVENTION_STATUS) + { + ap_remove_output_filter(f); + return send_error_bucket(msr, f, it); + } + } + + return ap_pass_brigade(f->next, bb_in); +} + diff --git a/src/msc_filters.h b/src/msc_filters.h new file mode 100644 index 0000000..1449108 --- /dev/null +++ b/src/msc_filters.h @@ -0,0 +1,24 @@ + +#include +#include +#include + +#include "http_core.h" +#include "http_request.h" +#include "httpd.h" +#include "ap_release.h" + +#include +#include + +#ifndef _SRC_MSC_FILTERS__ +#define _SRC_MSC_FILTERS__ + +#include "mod_security3.h" + +apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in); + +apr_status_t input_filter(ap_filter_t *f, apr_bucket_brigade *bb_out, + ap_input_mode_t mode, apr_read_type_e block, apr_off_t nbytes); + +#endif /* _SRC_MSC_FILTERS__ */ diff --git a/src/msc_utils.c b/src/msc_utils.c new file mode 100644 index 0000000..1b4d16c --- /dev/null +++ b/src/msc_utils.c @@ -0,0 +1,60 @@ + +#include "msc_utils.h" + + +int id(const char *fn, const char *format, ...) +{ + va_list args; + va_start(args, format); + FILE *f = fopen(fn, "a"); + vfprintf(f, format, args); + fclose(f); + va_end(args); +} + + +/** + * Sends a brigade with an error bucket down the filter chain. + */ +apr_status_t send_error_bucket(msc_t *msr, ap_filter_t *f, int status) +{ + apr_bucket_brigade *brigade = NULL; + apr_bucket *bucket = NULL; + + /* Set the status line explicitly for the error document */ + f->r->status_line = ap_get_status_line(status); + + brigade = apr_brigade_create(f->r->pool, f->r->connection->bucket_alloc); + if (brigade == NULL) + { + return APR_EGENERAL; + } + + bucket = ap_bucket_error_create(status, NULL, f->r->pool, + f->r->connection->bucket_alloc); + if (bucket == NULL) + { + return APR_EGENERAL; + } + + APR_BRIGADE_INSERT_TAIL(brigade, bucket); + + bucket = apr_bucket_eos_create(f->r->connection->bucket_alloc); + if (bucket == NULL) + { + return APR_EGENERAL; + } + + APR_BRIGADE_INSERT_TAIL(brigade, bucket); + + ap_pass_brigade(f->next, brigade); + + /* NOTE: + * It may not matter what we do from the filter as it may be too + * late to even generate an error (already sent to client). Nick Kew + * recommends to return APR_EGENERAL in hopes that the handler in control + * will notice and do The Right Thing. So, that is what we do now. + */ + return APR_EGENERAL; +} + diff --git a/src/msc_utils.h b/src/msc_utils.h new file mode 100644 index 0000000..366cbc6 --- /dev/null +++ b/src/msc_utils.h @@ -0,0 +1,26 @@ + +#include +#include +#include + +#include "http_core.h" +#include "http_request.h" +#include "httpd.h" +#include "ap_release.h" + +#include +#include + +#ifndef _SRC_MSC_UTILS__ +#define _SRC_MSC_UTILS__ + +#include "mod_security3.h" + + +int id(const char *fn, const char *format, ...); + + +apr_status_t send_error_bucket(msc_t *msr, ap_filter_t *f, int status); + + +#endif /* _SRC_MSC_UTILS__ */ diff --git a/t/TEST.PL b/t/TEST similarity index 71% rename from t/TEST.PL rename to t/TEST index ca5cf8d..86aa60e 100755 --- a/t/TEST.PL +++ b/t/TEST @@ -1,6 +1,8 @@ #!/usr/bin/perl use strict; +use FindBin; +use lib "$FindBin::RealBin/.."; use warnings FATAL => 'all'; use Apache::TestRun; diff --git a/t/conf/extra.conf.in b/t/conf/extra.conf.in new file mode 100644 index 0000000..6518559 --- /dev/null +++ b/t/conf/extra.conf.in @@ -0,0 +1,63 @@ + + +CoreDumpDirectory /tmp/ + +Include @ServerRoot@/.././t/conf/modules.conf + +LoadModule security3_module "@ServerRoot@/.././src/.libs/mod_security3.so" + + +# Lets make sure that the engine is on. +modsecurity_rules 'SecRuleEngine On' + +# Debug logs +modsecurity_rules 'SecDebugLog @ServerRoot@/logs/debug_logs.txt' +modsecurity_rules 'SecDebugLogLevel 9' + + + + modsecurity_rules 'SecRule REQUEST_URI "evil" "phase:1,id:111,log,status:403,block,deny"' + + + + modsecurity_rules 'SecRequestBodyAccess On' + modsecurity_rules 'SecRule ARGS "evil" "phase:2,id:112,log,status:403,block,deny"' + + + + modsecurity_rules 'SecRule ARGS "evil" "phase:3,id:113,log,status:403,block,deny"' + + + + modsecurity_rules 'SecResponseBodyAccess On' + modsecurity_rules 'SecRule ARGS "evil" "phase:4,id:114,log,status:403,block,deny"' + + + + modsecurity_rules 'SecRule ARGS "evil" "phase:5,id:115,log,status:403,block,deny"' + + + + + + modsecurity_rules 'SecRule REQUEST_URI "evil" "phase:1,id:111,log,status:402,block,deny"' + + + + modsecurity_rules 'SecRequestBodyAccess On' + modsecurity_rules 'SecRule ARGS "evil" "phase:2,id:112,log,status:402,block,deny"' + + + + modsecurity_rules 'SecRule ARGS "evil" "phase:3,id:113,log,status:402,block,deny"' + + + + modsecurity_rules 'SecResponseBodyAccess On' + modsecurity_rules 'SecRule ARGS "evil" "phase:4,id:1134,log,status:402,block,deny"' + + + + modsecurity_rules 'SecRule ARGS "evil" "phase:5,id:114,log,status:402,block,deny"' + + diff --git a/t/htdocs/block-evil-1-loc/evil b/t/htdocs/block-evil-1-loc/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-1-loc/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-1-loc/index.html b/t/htdocs/block-evil-1-loc/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-1-loc/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-1/evil b/t/htdocs/block-evil-1/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-1/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-1/index.html b/t/htdocs/block-evil-1/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-1/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-2-loc/evil b/t/htdocs/block-evil-2-loc/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-2-loc/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-2-loc/index.html b/t/htdocs/block-evil-2-loc/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-2-loc/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-2/evil b/t/htdocs/block-evil-2/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-2/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-2/evil-2.pl b/t/htdocs/block-evil-2/evil-2.pl new file mode 100644 index 0000000..c6284d2 --- /dev/null +++ b/t/htdocs/block-evil-2/evil-2.pl @@ -0,0 +1 @@ +whee diff --git a/t/htdocs/block-evil-2/index.html b/t/htdocs/block-evil-2/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-2/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-3-loc/evil b/t/htdocs/block-evil-3-loc/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-3-loc/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-3-loc/index.html b/t/htdocs/block-evil-3-loc/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-3-loc/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-3/asdf.html b/t/htdocs/block-evil-3/asdf.html new file mode 100644 index 0000000..6e577c4 --- /dev/null +++ b/t/htdocs/block-evil-3/asdf.html @@ -0,0 +1 @@ +felipe diff --git a/t/htdocs/block-evil-3/evil b/t/htdocs/block-evil-3/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-3/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-3/index.html b/t/htdocs/block-evil-3/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-3/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-4-loc/evil b/t/htdocs/block-evil-4-loc/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-4-loc/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-4-loc/index.html b/t/htdocs/block-evil-4-loc/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-4-loc/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-4/evil b/t/htdocs/block-evil-4/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-4/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-4/index.html b/t/htdocs/block-evil-4/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-4/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-5-loc/evil b/t/htdocs/block-evil-5-loc/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-5-loc/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-5-loc/index.html b/t/htdocs/block-evil-5-loc/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-5-loc/index.html @@ -0,0 +1 @@ +index diff --git a/t/htdocs/block-evil-5/evil b/t/htdocs/block-evil-5/evil new file mode 100644 index 0000000..53c74cd --- /dev/null +++ b/t/htdocs/block-evil-5/evil @@ -0,0 +1 @@ +evil diff --git a/t/htdocs/block-evil-5/index.html b/t/htdocs/block-evil-5/index.html new file mode 100644 index 0000000..9015a7a --- /dev/null +++ b/t/htdocs/block-evil-5/index.html @@ -0,0 +1 @@ +index diff --git a/t/load-modsec.t b/t/load-modsec.t new file mode 100644 index 0000000..4dbd9a3 --- /dev/null +++ b/t/load-modsec.t @@ -0,0 +1,13 @@ + +use strict; +use Apache::Test; +use Apache::TestRequest; +use FileHandle; + +plan tests => 1; + + +my $body = GET_BODY "/test.html"; +ok($body, qr/test/); + + diff --git a/t/simple-block.t b/t/simple-block.t new file mode 100644 index 0000000..a95a12e --- /dev/null +++ b/t/simple-block.t @@ -0,0 +1,40 @@ + +use strict; +use Apache::Test; +use Apache::TestRequest; +use FileHandle; +use Data::Dumper; + +plan tests => 11; + +my $res = GET "/index.html?evil=evil"; +ok $res->code == 200; + +my $res1 = GET "/block-evil-1/evil?evil=evil"; +ok $res1->code == 403; +my $res2 = GET "/block-evil-2/evil?evil=evil"; +ok $res2->code == 403; +my $res3 = GET "/block-evil-3/evil?evil=evil"; +ok $res3->code == 403; +my $res4 = GET "/block-evil-4/evil?evil=evil"; +ok $res4->code == 403; + +# Too late to block. +my $res5 = GET "/block-evil-5/evil?evil=evil"; +ok $res5->code == 200; + + +my $res1l = GET "/block-evil-1-loc/evil?evil=evil"; +ok $res1l->code == 402; +my $res2l = GET "/block-evil-2-loc/evil?evil=evil"; +ok $res2l->code == 402; +my $res3l = GET "/block-evil-3-loc/evil?evil=evil"; +ok $res3l->code == 402; +my $res4l = GET "/block-evil-4-loc/evil?evil=evil"; +ok $res4l->code == 402; + +# Too late to block. +my $res5l = GET "/block-evil-5-loc/evil?evil=evil"; +ok $res5l->code == 200; + + diff --git a/tests/.deps/msc_test-msc_test.Po b/tests/.deps/msc_test-msc_test.Po new file mode 100644 index 0000000..9ce06a8 --- /dev/null +++ b/tests/.deps/msc_test-msc_test.Po @@ -0,0 +1 @@ +# dummy diff --git a/tests/regression/action/00-disruptive-actions.t b/tests/regression/action/00-disruptive-actions.t new file mode 100644 index 0000000..64b86a3 --- /dev/null +++ b/tests/regression/action/00-disruptive-actions.t @@ -0,0 +1,445 @@ +### Tests all of the actions in each phase + +# Pass +{ + type => "action", + comment => "pass in phase:1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,pass,id:500033" + SecAction "phase:1,deny,id:500034" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,pass,id:500035" + SecAction "phase:2,deny,id:500036" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,pass,id:500037" + SecAction "phase:3,deny,id:500038" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,pass,id:500039" + SecAction "phase:4,deny,id:500040" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Allow +{ + type => "action", + comment => "allow in phase:1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,allow,id:500041" + SecAction "phase:1,deny,id:500042" + ), + match_log => { + error => [ qr/ModSecurity: Access allowed \(phase 1\). Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,allow,id:500043" + SecAction "phase:2,deny,id:500044" + ), + match_log => { + error => [ qr/ModSecurity: Access allowed \(phase 2\). Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,allow,id:500045" + SecAction "phase:3,deny,id:500046" + ), + match_log => { + error => [ qr/ModSecurity: Access allowed \(phase 3\). Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,allow,id:500047" + SecAction "phase:4,deny,id:500048" + ), + match_log => { + error => [ qr/ModSecurity: Access allowed \(phase 4\). Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Deny +{ + type => "action", + comment => "deny in phase:1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,deny,id:500049" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 1\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,deny,id:500050" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,deny,id:500051" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 3\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,deny,id:500052" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 4\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Drop +{ + type => "action", + comment => "drop in phase:1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,drop,id:500053" + ), + match_log => { + error => [ qr/Access denied with connection close \(phase 1\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,drop,id:500054" + ), + match_log => { + error => [ qr/Access denied with connection close \(phase 2\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,drop,id:500055" + ), + match_log => { + error => [ qr/Access denied with connection close \(phase 3\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,drop,id:500056" + ), + match_log => { + error => [ qr/Access denied with connection close \(phase 4\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Redirect +{ + type => "action", + comment => "redirect in phase:1 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:1,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',id:500001" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 1\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:2 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:2,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',id:500002" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 2\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:3 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecRule REQUEST_URI "\@streq /test2.txt" "phase:3,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',id:500003" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 3\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:4 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecRule REQUEST_URI "\@streq /test2.txt" "phase:4,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',id:500004" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 4\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, + + diff --git a/tests/regression/action/00-meta.t b/tests/regression/action/00-meta.t new file mode 100644 index 0000000..9e7b92a --- /dev/null +++ b/tests/regression/action/00-meta.t @@ -0,0 +1,8 @@ +### Test meta actions + +# TODO: id +# TODO: logdata +# TODO: msg +# TODO: rev +# TODO: severity +# TODO: tag diff --git a/tests/regression/action/00-misc.t b/tests/regression/action/00-misc.t new file mode 100644 index 0000000..1629d69 --- /dev/null +++ b/tests/regression/action/00-misc.t @@ -0,0 +1,22 @@ +### Test misc actions + +# TODO: block +# TODO: capture +# TODO: chain +# TODO: deprecatevar +# TODO: exec +# TODO: expirevar +# TODO: initcol +# TODO: multiMatch +# TODO: pause +# TODO: sanitiseArg +# TODO: sanitiseMatched +# TODO: sanitiseRequestHeader +# TODO: sanitiseResponseHeader +# TODO: setuid +# TODO: setsid +# TODO: setenv +# TODO: setvar +# TODO: skip +# TODO: skipAfter +# TODO: xmlns diff --git a/tests/regression/action/00-transformations.t b/tests/regression/action/00-transformations.t new file mode 100644 index 0000000..e8f7bb2 --- /dev/null +++ b/tests/regression/action/00-transformations.t @@ -0,0 +1,8 @@ +### Transformation tests + +# NOTE: individual tests done in unit tests + +# TODO: t:none to override default +# TODO: t:none inline +# TODO: combined +# TODO: caching diff --git a/tests/regression/action/10-ctl.t b/tests/regression/action/10-ctl.t new file mode 100644 index 0000000..a4940a3 --- /dev/null +++ b/tests/regression/action/10-ctl.t @@ -0,0 +1,56 @@ +### ctl + +### ruleRemoveById +{ + type => "action", + comment => "ruleRemoveById existing rule across phases", + conf => qq( + SecRuleEngine On + SecAction "phase:2,id:666,deny" + SecAction "phase:1,pass,ctl:ruleRemoveById=666,id:500030" + ), + match_log => { + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "ruleRemoveById future rule across phases", + conf => qq( + SecRuleEngine On + SecAction "phase:1,pass,ctl:ruleRemoveById=666,id:500031" + SecAction "phase:2,id:666,deny" + ), + match_log => { + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "ruleRemoveById future rule same phase", + conf => qq( + SecRuleEngine On + SecAction "phase:1,pass,ctl:ruleRemoveById=666,id:500032" + SecAction "phase:1,id:666,deny" + ), + match_log => { + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + + diff --git a/tests/regression/action/10-detectiononly-actions.t b/tests/regression/action/10-detectiononly-actions.t new file mode 100644 index 0000000..77e74e0 --- /dev/null +++ b/tests/regression/action/10-detectiononly-actions.t @@ -0,0 +1,455 @@ +### Tests all of the actions in each phase in detection only mode + +# Pass +{ + type => "action", + comment => "pass in phase:1", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAction "phase:1,pass,msg:'PASSED',id:500057" + SecAction "phase:1,deny,msg:'DENIED',id:500058" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*PASSED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:2", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,pass,msg:'PASSED',id:500059" + SecAction "phase:2,deny,msg:'DENIED',id:500060" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*PASSED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:3", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,pass,msg:'PASSED',id:500061" + SecAction "phase:3,deny,msg:'DENIED',id:500062" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*PASSED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:4", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,pass,msg:'PASSED',id:500063" + SecAction "phase:4,deny,msg:'DENIED',id:500064" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*PASSED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Allow +{ + type => "action", + comment => "allow in phase:1", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,allow,msg:'ALLOWED',id:500065" + SecAction "phase:1,deny,msg:'DENIED',id:500066" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*ALLOWED/, 1 ], + -error => [ qr/Access allowed/, 1 ], +# TODO: Allow should probably stop rule execution +# -error => [ qr/DENIED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:2", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,allow,msg:'ALLOWED',id:500067" + SecAction "phase:2,deny,msg:'DENIED',id:500068" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*ALLOWED/, 1 ], + -error => [ qr/Access allowed/, 1 ], +# TODO: Allow should probably stop rule execution +# -error => [ qr/DENIED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:3", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:3,allow,msg:'ALLOWED',id:500069" + SecAction "phase:3,deny,msg:'DENIED',id:500070" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*ALLOWED/, 1 ], + -error => [ qr/Access allowed/, 1 ], +# TODO: Allow should probably stop rule execution +# -error => [ qr/DENIED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:4", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:4,allow,msg:'ALLOWED',id:500071" + SecAction "phase:4,deny,msg:'DENIED',id:500072" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*ALLOWED/, 1 ], + -error => [ qr/Access allowed/, 1 ], +# TODO: Allow should probably stop rule execution +# -error => [ qr/DENIED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Deny +{ + type => "action", + comment => "deny in phase:1", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,deny,msg:'DENIED',id:500073" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DENIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:2", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,deny,msg:'DENIED',id:500074" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DENIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:3", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:3,deny,msg:'DENIED',id:500075" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DENIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:4", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:4,deny,msg:'DENIED',id:500076" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DENIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Drop +{ + type => "action", + comment => "drop in phase:1", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,drop,msg:'DROPPED',id:500077" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DROPPED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:2", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,drop,msg:'DROPPED',id:500078" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DROPPED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:3", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:3,drop,msg:'DROPPED',id:500079" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DROPPED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:4", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:4,drop,msg:'DROPPED',id:500080" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DROPPED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Redirect +{ + type => "action", + comment => "redirect in phase:1 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:1,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'REDIRECTED',id:500009" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*REDIRECTED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:2 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:2,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'REDIRECTED',id:500010" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*REDIRECTED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:3 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:3,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'REDIRECTED',id:500011" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*REDIRECTED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:4 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:4,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'REDIRECTED',id:500012" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*REDIRECTED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, + + diff --git a/tests/regression/action/10-logging.t b/tests/regression/action/10-logging.t new file mode 100644 index 0000000..b7e8061 --- /dev/null +++ b/tests/regression/action/10-logging.t @@ -0,0 +1,564 @@ +### Logging tests + +# log/nolog (pass) +{ + type => "action", + comment => "log (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log,id:500006" + ), + match_log => { + error => [ qr/ModSecurity: Warning\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog,id:500007" + ), + match_log => { + -error => [ qr/500007/, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# log/nolog (deny) +{ + type => "action", + comment => "log (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,log,id:500008" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,nolog,id:500009" + ), + match_log => { + -error => [ qr/500009/, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# auditlog/noauditlog (pass) +{ + type => "action", + comment => "auditlog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog,id:500010" + ), + match_log => { + error => [ qr/ModSecurity: Warning\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog,id:500011" + ), + match_log => { + error => [ qr/ModSecurity: Warning\. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# auditlog/noauditlog (deny) +{ + type => "action", + comment => "auditlog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,auditlog,id:500012" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,noauditlog,id:500013" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# All log/nolog auditlog/noauditlog combos (pass) +{ + type => "action", + comment => "log,auditlog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log,auditlog,id:500014" + ), + match_log => { + error => [ qr/ModSecurity: Warning\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "log,noauditlog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log,noauditlog,id:500015" + ), + match_log => { + error => [ qr/ModSecurity: Warning\. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog,auditlog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog,auditlog,id:500016" + ), + match_log => { + audit => [ qr/-H--\s+Message: .*Stopwatch: /s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog,noauditlog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog,noauditlog,id:500017" + ), + match_log => { + -error => [ qr/500017/, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "auditlog,log (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog,log,id:500018" + ), + match_log => { + error => [ qr/ModSecurity: Warning\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "auditlog,nolog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog,nolog,id:500019" + ), + match_log => { + -error => [ qr/500019/, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog,log (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog,log,id:500020" + ), + match_log => { + error => [ qr/ModSecurity: Warning\. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog,nolog (pass)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog,nolog,id:500021" + ), + match_log => { + -error => [ qr/500021/, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# All log/nolog auditlog/noauditlog combos (deny) +{ + type => "action", + comment => "log,auditlog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,log,auditlog,id:500022" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "log,noauditlog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,log,noauditlog,id:500023" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog,auditlog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,nolog,auditlog,id:500024" + ), + match_log => { + audit => [ qr/-H--\s+Message: .*Stopwatch: /s, 1 ], + }, + match_response => { + -error => [ qr/ModSecurity: /, 1 ], + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog,noauditlog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,nolog,noauditlog,id:500025" + ), + match_log => { + -error => [ qr/500025/, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "auditlog,log (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,auditlog,log,id:500026" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "auditlog,nolog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,auditlog,nolog,id:500027" + ), + match_log => { + -error => [ qr/500027/, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog,log (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,noauditlog,log,id:500028" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 403 \(phase 1\)\. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog,nolog (deny)", + conf => qq( + SecRuleEngine On + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAuditLogRelevantStatus xxx + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,deny,status:403,noauditlog,nolog,id:500029" + ), + match_log => { + -error => [ qr/500029/, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, diff --git a/tests/regression/config/00-load-modsec.t b/tests/regression/config/00-load-modsec.t new file mode 100644 index 0000000..49a4bee --- /dev/null +++ b/tests/regression/config/00-load-modsec.t @@ -0,0 +1,31 @@ +{ + type => "config", + comment => "module loaded", + match_log => { + error => { + apache_v3 => [ qr/ModSecurity-Apache/, 10 ], + apache => [ qr/ModSecurity for Apache.* configured\./, 10 ], + nginx => [ qr/ModSecurity for nginx.* configured\./, 10 ], + }, + }, +}, +{ + type => "config", + comment => "minimal config", + conf => sub { + # Open the minimal conf file, substituting the + # relative log paths with full paths. + open(C, "<$ENV{DIST_ROOT}/modsecurity.conf-minimal") or die "$!\n"; + (my $conf = join('', )) =~ s#Log logs/#Log $ENV{TEST_SERVER_ROOT}/logs/#g; + close C; + + return $conf; + }, + match_log => { + error => { + apache_v3 => [ qr/ModSecurity-Apache/, 10 ], + apache => [ qr/ModSecurity for Apache.* configured\./, 10 ], + nginx => [ qr/ModSecurity for nginx.* configured\./, 10 ], + }, + }, +}, diff --git a/tests/regression/config/10-audit-directives.t b/tests/regression/config/10-audit-directives.t new file mode 100644 index 0000000..7d1f458 --- /dev/null +++ b/tests/regression/config/10-audit-directives.t @@ -0,0 +1,269 @@ +### SecAudit* directive tests + +# SecAuditEngine +{ + type => "config", + comment => "SecAuditEngine On", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + ), + match_log => { + audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecAuditEngine Off", + conf => qq( + SecAuditEngine Off + SecAuditLog $ENV{AUDIT_LOG} + ), + match_log => { + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecAuditEngine RelevantOnly (pos)", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog $ENV{AUDIT_LOG} + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecResponseBodyAccess On + SecDefaultAction "phase:2,log,auditlog,pass" + SecRule REQUEST_URI "." "phase:4,deny,id:500251" + ), + match_log => { + audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecAuditEngine RelevantOnly (neg)", + conf => qq( + SecAuditEngine RelevantOnly + SecAuditLog $ENV{AUDIT_LOG} + SecResponseBodyAccess On + SecDefaultAction "phase:2,log,auditlog,pass" + ), + match_log => { + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecAuditLogType & SecAuditLogStorageDir +{ + type => "config", + comment => "SecAuditLogType Serial", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecAuditLogType Serial + ), + match_log => { + audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/bogus", + ), +}, +{ + type => "config", + comment => "SecAuditLogType Concurrent", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecAuditLogType Concurrent + SecAuditLogStorageDir "$ENV{LOGS_DIR}/audit" + ), + test => sub { + ### Perl code to parse the audit log entry and verify + ### that the concurrent audit log exists and contains + ### the correct data. + ### + ### TODO: Need some API for this :) + ### + ### FIXME: Just workable with apache, the timing to load auditlog from nginx + ### is not correct, so the test is failing even when it should pass. + ### Disabling it for now until we figure out a way to handle that. + + # Parse log + #my $alogre = qr/^(?:\S+)\ (?:\S+)\ (?:\S+)\ (?:\S+)\ \[(?:[^:]+):(?:\d+:\d+:\d+)\ (?:[^\]]+)\]\ \"(?:.*)\"\ (?:\d+)\ (?:\S+)\ \"(?:.*)\"\ \"(?:.*)\"\ (\S+)\ \"(?:.*)\"\ (\S+)\ (?:\d+)\ (?:\d+)\ (?:\S+)(?:.*)$/m; + #my $alog = match_log("audit", $alogre, 1); + #chomp $alog; + #dbg("Alog: $alog\n"); + #my @log = ($alog =~ m/$alogre/); + #my($id, $fn) = ($log[0], $log[1]); + #if (!$id or !$fn) { + #dbg("LOG ENTRY: $alog"); + #die "Failed to parse audit log: $ENV{AUDIT_LOG}\n"; + #} + + # Verify concurrent log exists + #my $alogdatafn = "$ENV{LOGS_DIR}/audit$fn"; + #if (! -e "$alogdatafn") { + #die "Audit log does not exist: $alogdatafn\n"; + #} + + # Verify concurrent log contents + #if (defined match_file($alogdatafn, qr/^--[^-]+-A--.*$id.*-Z--$/s)) { + #return 0; + #} + + # Error + #dbg("LOGDATA: \"$FILE{$alogdatafn}{buf}\""); + #die "Audit log data did not match.\n"; + return 0; + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecAuditLogRelevantStatus +{ + type => "config", + comment => "SecAuditLogRelevantStatus (pos)", + conf => qq( + SecAuditEngine RelevantOnly + SecAuditLog $ENV{AUDIT_LOG} + SecAuditLogRelevantStatus "^4" + ), + match_log => { + audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/bogus", + ), +}, +{ + type => "config", + comment => "SecAuditLogRelevantStatus (neg)", + conf => qq( + SecAuditEngine RelevantOnly + SecAuditLog $ENV{AUDIT_LOG} + SecAuditLogRelevantStatus "^4" + ), + match_log => { + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecAuditLogParts +{ + type => "config", + comment => "SecAuditLogParts (minimal)", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecRequestBodyAccess On + SecResponseBodyAccess On + SecAuditLogParts "AZ" + ), + match_log => { + audit => [ qr/-A--.*-Z--/s, 1 ], + -audit => [ qr/-[B-Y]--/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1r&=2", + ), +}, +{ + type => "config", + comment => "SecAuditLogParts (default)", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecRequestBodyAccess On + SecResponseBodyAccess On + ), + match_log => { + audit => [ qr/-A--.*-B--.*-F--.*-H--.*-Z--/s, 1 ], + -audit => [ qr/-[DEGIJK]--/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1r&=2", + ), +}, +{ + type => "config", + comment => "SecAuditLogParts (all)", + conf => qq( + SecRuleEngine On + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecRequestBodyAccess On + SecResponseBodyAccess On + SecAuditLogParts "ABCDEFGHIJKZ" + SecAction "phase:4,log,auditlog,allow,id:500086" + ), + match_log => { + audit => [ qr/-A--.*-B--.*-C--.*-F--.*-E--.*-H--.*-K--.*-Z--/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1r&=2", + ), +}, diff --git a/tests/regression/config/10-debug-directives.t b/tests/regression/config/10-debug-directives.t new file mode 100644 index 0000000..5d78662 --- /dev/null +++ b/tests/regression/config/10-debug-directives.t @@ -0,0 +1,269 @@ +### SecDebug* directive tests +{ + type => "config", + comment => "SecDebugLog (pos)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + ), + match_log => { + debug => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecDebugLog (neg)", + conf => qq( + SecRuleEngine On + ), + match_log => { + -debug => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 0", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 0 + SecRule REQUEST_URI "." "phase:1,deny,id:500241" + ), + match_log => { + -debug => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 1", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 1 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500242" + ), + match_log => { + debug => [ qr/\]\[[1]\] /, 1 ], + -debug => [ qr/\]\[[2-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 2", + conf => qq( + SecRuleEngine DetectionOnly + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 2 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500243" + ), + match_log => { + debug => [ qr/\]\[2\] /, 1 ], + -debug => [ qr/\]\[[3-9]\] /, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 3", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 3 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500244" + ), + match_log => { + debug => [ qr/\]\[3\] /, 1 ], + -debug => [ qr/\]\[[4-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 4", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500245" + ), + match_log => { + debug => [ qr/\]\[4\] /, 1 ], + -debug => [ qr/\]\[[5-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 5", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500246" + ), + match_log => { + debug => [ qr/\]\[5\] /, 1 ], + -debug => [ qr/\]\[[6-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 6", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 6 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500247" + ), + match_log => { + debug => [ qr/\]\[6\] /, 1 ], + -debug => [ qr/\]\[[7-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 7", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 7 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500248" + ), + match_log => { + debug => [ qr/\]\[7\] /, 1 ], + -debug => [ qr/\]\[[8-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 8", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 8 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500249" + ), + match_log => { + debug => [ qr/\]\[8\] /, 1 ], + -debug => [ qr/\]\[9\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 9", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "(.)" "phase:4,deny,id:500250" + ), + match_log => { + debug => [ qr/\]\[9\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, diff --git a/tests/regression/config/10-misc-directives.t b/tests/regression/config/10-misc-directives.t new file mode 100644 index 0000000..86c1aed --- /dev/null +++ b/tests/regression/config/10-misc-directives.t @@ -0,0 +1,105 @@ +### Misc directive tests + +### TODO: +# SecTmpDir +# SecUploadKeepFiles +# SecChrootDir +# SecGuardianLog + +# SecDefaultAction +{ + type => "config", + comment => "SecDefaultAction", + conf => qq( + SecRuleEngine on + SecDefaultAction "phase:1,deny,status:500" + SecRule REQUEST_URI "test.txt" "id:500241" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 500 \(phase 1\)/, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + + +# SecDataDir +{ + type => "config", + comment => "SecDataDir", + conf => qq( + SecRuleEngine On + SecDataDir "$ENV{DATA_DIR}" + SecAction initcol:ip=%{REMOTE_ADDR},setvar:ip.dummy=1,pass,id:500085 + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_file => { + "$ENV{DATA_DIR}/ip.pag" => qr/\x00\x06dummy\x00\x00\x021\x00/, + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecTmpDir/SecUploadDir/SecUploadKeepFiles +{ + type => "config", + comment => "SecTmpDir/SecUploadDir/SecUploadKeepFiles", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecTmpDir "$ENV{TEMP_DIR}" + SecUploadKeepFiles On + SecUploadDir "$ENV{UPLOAD_DIR}" + ), + test => sub { + # Get the filename and make sure the file exists + my $fn = match_log(debug => qr/Moved file from .* to ".*"\./, 5); + die "Failed to determine uploaded filename\n" unless (defined $fn); + + $fn =~ s/Moved file from .* to "(.*)"\..*/$1/; + die "File does not exist: $fn\n" unless (-e $fn); + + # Check the contents of the file + return 0 if (match_file($fn, qr/^TESTFILE$/m)); + + msg("Failed to match contents of uploaded file: $fn"); + return 1; + }, + match_log => { + debug => [ qr/Created temporary file.*$ENV{TEMP_DIR}/, 1 ], + -debug => [ qr/Failed to /, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------19813181771830765643996187206", + ], + q(-----------------------------19813181771830765643996187206 +Content-Disposition: form-data; name="upload-file"; filename="test" +Content-Type: application/octet-stream + +TESTFILE +-----------------------------19813181771830765643996187206 +Content-Disposition: form-data; name="file" + +Upload File +-----------------------------19813181771830765643996187206--), + ), +}, + + diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t new file mode 100644 index 0000000..42094e1 --- /dev/null +++ b/tests/regression/config/10-request-directives.t @@ -0,0 +1,705 @@ +### Tests for directives altering how a request is handled + +# SecArgumentSeparator +{ + type => "config", + comment => "SecArgumentSeparator (get-pos)", + conf => q( + SecRuleEngine On + SecArgumentSeparator ";" + SecRule ARGS:a "@streq 1" "phase:1,deny,chain,id:500215" + SecRule ARGS:b "@streq 2" "" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 1\)\. String match "2" at ARGS:b\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?a=1;b=2", + ), +}, +{ + type => "config", + comment => "SecArgumentSeparator (get-neg)", + conf => q( + SecRuleEngine On + SecRule ARGS:a "@streq 1" "phase:1,deny,chain,id:500217" + SecRule ARGS:b "@streq 2" "" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?a=1;b=2", + ), +}, +{ + type => "config", + comment => "SecArgumentSeparator (post-pos)", + conf => q( + SecRuleEngine On + SecRequestBodyAccess On + SecArgumentSeparator ";" + SecRule ARGS:a "@streq 1" "phase:2,deny,chain,id:500219" + SecRule ARGS:b "@streq 2" "" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\)\. String match "2" at ARGS:b\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1;b=2", + ), +}, +{ + type => "config", + comment => "SecArgumentSeparator (post-neg)", + conf => q( + SecRuleEngine On + SecRequestBodyAccess On + SecRule ARGS:a "@streq 1" "phase:2,deny,id:500221" + SecRule ARGS:b "@streq 2" "phase:2,deny,id:500222" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1;b=2", + ), +}, + +# SecRequestBodyAccess +{ + type => "config", + comment => "SecRequestBodyAccess (pos)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRule ARGS:a "\@streq 1" "phase:2,deny,chain,id:500223" + SecRule ARGS:b "\@streq 2" "" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\)\. String match "2" at ARGS:b\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecRequestBodyAccess (neg)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess Off + SecRule ARGS:a "\@streq 1" "phase:2,deny,id:500225" + SecRule ARGS:b "\@streq 2" "phase:2,deny,id:500226" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, + +# SecRequestBodyLimit +{ + type => "config", + comment => "SecRequestBodyLimit (equal)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 7 + ), + match_log => { + -error => [ qr/Request body is larger than the configured limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (greater)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 5 + ), + match_log => { + error => [ qr/Request body .*is larger than the configured limit \(5\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (equal - chunked)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 276 + ), + match_log => { + -error => [ qr/Request body is larger than the configured limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (greater - chunked)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 256 + ), + match_log => { + error => [ qr/Request body .*is larger than the configured limit \(256\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (ctl:ruleEngine=off)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 5 + + SecAction "phase:1,pass,nolog,ctl:ruleEngine=off,id:500081" + SecRule REQUEST_BODY "." "phase:2,deny,id:500227" + ), + match_log => { + -error => [ qr/Request body .*is larger than the configured limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (ctl:requestBodyAccess=off)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 5 + + SecAction "phase:1,pass,nolog,ctl:requestBodyAccess=off,id:500082" + SecRule REQUEST_BODY "." "phase:2,deny,id:500228" + ), + match_log => { + -error => [ qr/Request body .*is larger than the configured limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (ctl:ruleEngine=off - chunked)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 256 + + SecAction "phase:1,pass,nolog,ctl:ruleEngine=off,id:500083" + SecRule REQUEST_BODY "." "phase:2,deny,id:500229" + ), + match_log => { + -error => [ qr/Request body .*is larger than the configured limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (ctl:requestBodyAccess=off - chunked)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 256 + + SecAction "phase:1,pass,nolog,ctl:requestBodyAccess=off,id:500084" + SecRule REQUEST_BODY "." "phase:2,deny,id:500230" + ), + match_log => { + -error => [ qr/Request body .*is larger than the configured limit \(256\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, + +# SecRequestBodyInMemoryLimit +{ + type => "config", + comment => "SecRequestBodyInMemoryLimit (equal)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimit 1000 + SecRequestBodyInMemoryLimit 276 + ), + match_log => { + -debug => [ qr/Input filter: Request too large to store in memory, switching to disk\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, +{ + type => "config", + comment => "SecRequestBodyInMemoryLimit (greater)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimit 1000 + SecRequestBodyInMemoryLimit 16 + ), + match_log => { + debug => [ qr/Input filter: Request too large to store in memory, switching to disk\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (multipart/greater - chunked)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyLimit 20 + ), + match_log => { + debug => [ qr/Request body is larger than the configured limit \(20\).. Deny with code \(413\)/, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (plain/greater)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyLimit 131072 + ), + match_log => { + -debug => [ qr/Request body is larger than the configured limit \(131072\).. Deny with code \(413\)/, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q( + { + ) . "'abcdefghijlmnopq'='abcdefghijlmnopqrstuvxz',\\n" x 99000 . q( + }, + ), + ), + ), +}, + + +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/greater - chunked)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 131072 + ), + match_log => { + -debug => [ qr/Request body is larger than the configured limit/, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1) . "a" x 131072 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2) . "b" x 131072 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 131072*3 + ), +}, +# Known issue on nginx, disable it for now. +#{ +# type => "config", +# comment => "SecRequestBodyLimitAction ProcessPartial (plain/greater)", +# conf => qq( +# SecRuleEngine On +# SecDebugLog $ENV{DEBUG_LOG} +# SecDebugLogLevel 9 +# SecRequestBodyAccess On +# SecRequestBodyLimitAction ProcessPartial +# SecRequestBodyLimit 131072 +# ), +# match_log => { +# -debug => [ qr/Request body is larger than the configured limit/, 1], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", +# [ +# "Content-Type" => "application/json", +# ], +# normalize_raw_request_data( +# q( +# { +# ) . "'abcdefghijlmnopq'='abcdefghijlmnopqrstuvxz',\\n" x 99000 . q( +# }, +# ), +# ), +# ), +#}, + + + + + +# SecCookieFormat +{ + type => "config", + comment => "SecCookieFormat (pos)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecCookieFormat 1 + SecRule REQUEST_COOKIES_NAMES "\@streq SESSIONID" "phase:1,deny,chain,id:500231" + SecRule REQUEST_COOKIES:\$SESSIONID_PATH "\@streq /" "chain" + SecRule REQUEST_COOKIES:SESSIONID "\@streq cookieval" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 1\)\. String match "cookieval" at REQUEST_COOKIES:SESSIONID\./, 1 ], + debug => [ qr(Adding request cookie: name "\$SESSIONID_PATH", value "/"), 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Cookie" => q($Version="1"; SESSIONID="cookieval"; $PATH="/"), + ], + undef, + ), +}, +{ + type => "config", + comment => "SecCookieFormat (neg)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecCookieFormat 0 + SecRule REQUEST_COOKIES_NAMES "\@streq SESSIONID" "phase:1,deny,chain,id:500234" + SecRule REQUEST_COOKIES:\$SESSIONID_PATH "\@streq /" "chain" + SecRule REQUEST_COOKIES:SESSIONID "\@streq cookieval" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + -debug => [ qr(Adding request cookie: name "\$SESSIONID_PATH", value "/"), 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Cookie" => q($Version="1"; SESSIONID="cookieval"; $PATH="/"), + ], + undef, + ), +}, + diff --git a/tests/regression/config/10-response-directives.t b/tests/regression/config/10-response-directives.t new file mode 100644 index 0000000..51f10c4 --- /dev/null +++ b/tests/regression/config/10-response-directives.t @@ -0,0 +1,174 @@ +### Tests for directives altering how a response is handled + +# SecResponseBodyMimeTypesClear +{ + type => "config", + comment => "SecResponseBodyMimeTypesClear", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeTypesClear + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule RESPONSE_BODY "TEST" "phase:4,deny,id:500237" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + debug => [ qr/Not buffering response body for unconfigured MIME type/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecResponseBodyAccess & SecResponseBodyMimeType +{ + type => "config", + comment => "SecResponseBodyAccess On", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule RESPONSE_BODY "TEST" "phase:4,deny,id:500238" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 4\)\. Pattern match "TEST" at RESPONSE_BODY\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecResponseBodyAccess Off", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecResponseBodyAccess Off + SecResponseBodyMimeType text/plain null + SecRule RESPONSE_BODY "TEST" "phase:4,deny,id:500239" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + debug => [ qr/Response body buffering is not enabled\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecResponseBodyLimit +{ + type => "config", + comment => "SecResponseBodyLimit (equal)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 8192 + ), + match_log => { + -error => [ qr/Content-Length \(\d+\) over the limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, +{ + type => "config", + comment => "SecResponseBodyLimit (less)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 9000 + ), + match_log => { + -error => [ qr/Content-Length \(\d+\) over the limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, +{ + type => "config", + comment => "SecResponseBodyLimit (greater)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 8000 + ), + match_log => { + error => [ qr/Content-Length \(\d+\) over the limit \(8000\)\./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, + +# ResponseBodyLimitAction +{ + type => "config", + comment => "SecResponseBodyLimitAction Reject", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 5 + SecResponseBodyLimitAction Reject + ), + match_log => { + error => [ qr/Content-Length \(\d+\) over the limit \(5\)\./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, +{ + type => "config", + comment => "SecResponseBodyLimitAction ProcessPartial", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 5 + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecResponseBodyLimitAction ProcessPartial + ), + match_log => { + -error => [ qr/Content-Length \(\d+\) over the limit/, 1 ], + debug => [ qr/Processing partial response body \(limit 5\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, diff --git a/tests/regression/config/20-chroot.t b/tests/regression/config/20-chroot.t new file mode 100644 index 0000000..2147d2b --- /dev/null +++ b/tests/regression/config/20-chroot.t @@ -0,0 +1,35 @@ +### SecChroot tests +# TODO: Will not work as we need root access + +#{ +# type => "config", +# comment => "SecChroot", +# httpd_opts => qw( +# -DCHROOT +# ), +# conf => qq( +# # These will be in the chroot +# PidFile /logs/httpd.pid +# ScoreBoardFile /logs/httpd.scoreboard +# User nobody +# Group nogroup +# +# SecAuditEngine On +# SecDebugLog $ENV{DEBUG_LOG} +# SecDebugLogLevel 9 +# SecAuditLog $ENV{AUDIT_LOG} +# SecAuditLogStorageDir "/logs/audit" +# SecAuditLogType Concurrent +# SecChrootDir "$ENV{TEST_SERVER_ROOT}" +# ), +# match_log => { +# debug => [ qr/./, 1 ], +# audit => [ qr/./, 1 ], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", +# ), +#}, diff --git a/tests/regression/misc/00-multipart-parser.t b/tests/regression/misc/00-multipart-parser.t new file mode 100644 index 0000000..de39bf0 --- /dev/null +++ b/tests/regression/misc/00-multipart-parser.t @@ -0,0 +1,1813 @@ +### Multipart parser tests + +# Normal +{ + type => "misc", + comment => "multipart parser (normal)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500055" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500056" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500057" + ), + match_log => { + debug => [ qr/Added file part [0-9a-h]+ to the list: name "image" file name "image.jpg" \(offset 258, length 10\).*Adding request argument \(BODY\): name "name", value "Brian Rectanus".*Adding request argument \(BODY\): name "email", value "brian.rectanus\@breach.com"/s, 1 ], + -debug => [ qr/Multipart.*(?i:error|warning)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, + +# Final CRLF or not, we should still work +{ + type => "misc", + comment => "multipart parser (final CRLF)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500058" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500059" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500060" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "1".*Adding request argument \(BODY\): name "b", value "2"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, + +# No final CRLF +{ + type => "misc", + comment => "multipart parser (no final CRLF)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500061" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500062" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500063" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "1".*Adding request argument \(BODY\): name "b", value "2"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646--), + ), + ), +}, + +# Should work with a boundary of "boundary" +{ + type => "misc", + comment => "multipart parser (boundary contains \"boundary\")", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500064" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500065" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500066" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "1".*Adding request argument \(BODY\): name "b", value "2"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=------------------------------------------------boundary", + ], + normalize_raw_request_data( + q( + --------------------------------------------------boundary + Content-Disposition: form-data; name="a" + + 1 + --------------------------------------------------boundary + Content-Disposition: form-data; name="b" + + 2 + --------------------------------------------------boundary-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (boundary contains \"bOuNdArY\")", + note => q( + KHTML Boundary + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500067" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500068" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500069" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "1".*Adding request argument \(BODY\): name "b", value "2"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=--------0xKhTmLbOuNdArY", + ], + normalize_raw_request_data( + q( + ----------0xKhTmLbOuNdArY + Content-Disposition: form-data; name="a" + + 1 + ----------0xKhTmLbOuNdArY + Content-Disposition: form-data; name="b" + + 2 + ----------0xKhTmLbOuNdArY-- + ), + ), + ), +}, + +# We should handle data starting with a "--" +{ + type => "misc", + comment => "multipart parser (data contains \"--\")", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500070" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500071" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "--test".*Adding request argument \(BODY\): name "b", value "--"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + --test + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + -- + -----------------------------69343412719991675451336310646--), + ), + ), +}, + +# We should emit warnings for parsing errors +{ + type => "misc", + comment => "multipart parser error (no final boundary)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + ), + match_log => { + debug => [ qr/Final boundary missing/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser error (no disposition)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + ), + match_log => { + -debug => [ qr/Multipart error:/, 1 ], + audit => [ qr/Part missing Content-Disposition header/, 1 ], + debug => [ qr/Part missing Content-Disposition header/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + + 1 + -----------------------------69343412719991675451336310646 + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser error (bad disposition)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + ), + match_log => { + audit => [ qr/Invalid Content-Disposition header/, 1 ], + debug => [ qr/Invalid Content-Disposition header/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser error (no disposition name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + ), + match_log => { + -debug => [ qr/Multipart error:/, 1 ], + audit => [ qr/Content-Disposition header missing name field/, 1 ], + debug => [ qr/Content-Disposition header missing name field/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, + +# Zero length part name should not crash +{ + type => "misc", + comment => "multipart parser (zero length part name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "!\@eq 1" "phase:2,deny,id:500072" + SecRule REQBODY_PROCESSOR_ERROR "!\@eq 1" "phase:2,deny,id:500073" + ), + match_log => { + debug => [ qr/name: a.*variable: 1.*Invalid part header \(header name missing\)/s, 1 ], + -debug => [ qr/Adding request argument \(BODY\): name "b"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + : + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, + +# Part header folding +{ + type => "misc", + comment => "multipart parser (part header folding - space)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "!\@eq 1" "phase:2,deny,status:403,id:500074" + SecRule MULTIPART_HEADER_FOLDING "!\@eq 1" "phase:2,deny,status:403,id:500075" + SecRule MULTIPART_INVALID_HEADER_FOLDING "!\@eq 0" "phase:2,deny,status:403,id:500076" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,status:403,id:500077" + ), + match_log => { + debug => [ qr/name: a.*variable: 1.*name: b.*variable: 2/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (part header folding - tab)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "!\@eq 1" "phase:2,deny,status:403,id:500078" + SecRule MULTIPART_HEADER_FOLDING "!\@eq 1" "phase:2,deny,status:403,id:500079" + SecRule MULTIPART_INVALID_HEADER_FOLDING "!\@eq 0" "phase:2,deny,status:403,id:500080" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,status:403,id:500081" + ), + match_log => { + debug => [ qr/name: a.*variable: 1.*name: b.*variable: 2/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (part header folding - mixed)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "!\@eq 1" "phase:2,deny,status:403,id:500082" + SecRule MULTIPART_HEADER_FOLDING "!\@eq 1" "phase:2,deny,status:403,id:500083" + SecRule MULTIPART_INVALID_HEADER_FOLDING "!\@eq 0" "phase:2,deny,status:403,id:500084" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,status:403,id:500085" + ), + match_log => { + debug => [ qr/name: a.*variable: 1.*name: b.*variable: 2/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (part header folding - invalid)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "!\@eq 1" "phase:2,deny,status:403,id:500086" + SecRule MULTIPART_HEADER_FOLDING "!\@eq 1" "phase:2,deny,status:403,id:500087" + SecRule MULTIPART_INVALID_HEADER_FOLDING "!\@eq 1" "phase:2,deny,status:403,id:500088" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,status:403,id:500089" + ), + match_log => { + debug => [ qr/name: a.*variable: 1.*name: b.*variable: 2/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (part header folding - mixed invalid)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "!\@eq 1" "phase:2,deny,status:403,id:500090" + SecRule MULTIPART_HEADER_FOLDING "!\@eq 1" "phase:2,deny,status:403,id:500091" + SecRule MULTIPART_INVALID_HEADER_FOLDING "!\@eq 1" "phase:2,deny,status:403,id:500092" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,status:403,id:500093" + ), + match_log => { + debug => [ qr/name: a.*variable: 1.*name: b.*variable: 2/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, + +# Data following final boundary should set flag +{ + type => "misc", + comment => "multipart parser (data after final boundary)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_DATA_AFTER "\@eq 1" "phase:2,deny,status:403,id:500094" + ), + match_log => { + debug => [ qr/name: a.*variable: 1.*Ignoring data after last boundary/s, 1 ], + -debug => [ qr/Adding request argument \(BODY\): name "b"/s, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646-- + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, + +# Single quoted data is invalid +{ + type => "misc", + comment => "multipart parser (C-D uses single quotes)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "!\@eq 1" "phase:2,deny,id:500095" + SecRule MULTIPART_INVALID_QUOTING "!\@eq 1" "phase:2,deny,id:500096" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "chain,phase:2,deny,id:500097" + ), + match_log => { + debug => [ qr/name: a.*variable: 1.*Duplicate Content-Disposition name/s, 1 ], + -debug => [ qr/Adding request argument \(BODY\): name "b/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name=';filename="dummy';name=b;" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, + +# Invalid boundary separators +{ + type => "misc", + comment => "multipart parser (invalid C-T boundary separator - comma)", + note => q( + PHP 5.2.3 - no effect. + ), + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500098" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500099" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500100" + ), + match_log => { + error => [ qr/Invalid boundary in C-T \(malformed\)/, 1 ], + + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data, boundary=0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (invalid C-T boundary separator - space)", + note => q( + PHP 5.2.3 - no effect. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500101" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500102" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500103" + ), + match_log => { + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data boundary=0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, + +# Invalid boundary parameter names +{ + type => "misc", + comment => "multipart parser (invalid C-T boundary parameter name - case)", + note => q( + PHP 5.2.3 - does not recognise boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500104" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500105" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500106" + ), + match_log => { + error => [ qr/Invalid boundary in C-T \(case sensitivity\)/, 1 ], + + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; bOundAry=0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (invalid C-T boundary parameter name - trailing chars)", + note => q( + PHP 5.2.3 - does not recognise boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500107" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500108" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500109" + ), + match_log => { + error => [ qr/Invalid boundary in C-T \(parameter name\)/, 1 ], + + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary123=0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, + +# Invalid boundaries +{ + type => "misc", + comment => "multipart parser (multiple C-T boundaries - first quoted)", + note => q( + PHP 5.2.3 - uses first boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500110" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500111" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500112" + ), + match_log => { + debug => [ qr/Multiple boundary parameters in C-T/, 1 ], + + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary="0000"; boundary=1111), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (multiple C-T boundaries - comma separated)", + note => q( + PHP 5.2.3 - uses first boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500113" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500114" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500115" + ), + match_log => { + debug => [ qr/Multiple boundary parameters in C-T/, 1 ], + + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000, boundary=1111), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (boundary whitespace in C-T - after name)", + note => q( + PHP 5.2.3 - no effect. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500116" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500117" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500118" + ), + match_log => { + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary =0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (boundary whitespace in C-T - before value)", + note => q( + PHP 5.2.3 - uses " 0000" as boundary. + We should probably interpret it as "0000" and just flag a strict error. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500119" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500120" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500121" + ), + match_log => { + debug => [ qr/boundary whitespace in C-T header/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary= 0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (boundary whitespace in C-T - after value)", + note => q( + PHP 5.2.3 - no effect. + Apache just silently removes the trailing whitespace for us. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500122" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500123" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500124" + ), + match_log => { + debug => [ qr/Added file part [0-9a-h]+ to the list: name "image" file name "image.jpg" \(offset 258, length 10\).*Adding request argument \(BODY\): name "name", value "Brian Rectanus".*Adding request argument \(BODY\): name "email", value "brian.rectanus\@breach.com"/s, 1 ], + -debug => [ qr/Multipart.*(?i:error|warning)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000 ), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, + +# Special chars +{ + type => "misc", + comment => "multipart parser (boundary special char - trailing whitespace+token)", + note => q( + PHP 5.2.3 - uses "0000 1111" as boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500125" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500126" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500127" + ), + match_log => { + debug => [ qr/No boundaries found in payload/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000 1111), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (boundary special char - trailing comma+token)", + note => q( + PHP 5.2.3 - uses "0000" as boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500128" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500129" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500130" + ), + match_log => { + debug => [ qr/Invalid boundary in C-T \(characters\)/, 1 ], + + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000,1111), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, + +# Quoting +{ + type => "misc", + comment => "multipart parser (quoted boundary - normal)", + note => q( + PHP 5.2.3 - no effect. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500131" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500132" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500133" + ), + match_log => { + debug => [ qr/boundary was quoted/, 1 ], + + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary="0000"), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (quoted boundary value - whitespace before)", + note => q( + PHP 5.2.3 - uses " 0000" as boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500134" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500135" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500136" + ), + match_log => { + debug => [ qr/boundary was quoted.*No boundaries found in payload/s, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=" 0000"), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (quoted boundary value - whitespace after)", + note => q( + PHP 5.2.3 - uses "0000 " as boundary. + Trailing whitespace violates RFC as well. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500137" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500138" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500139" + ), + match_log => { + debug => [ qr/boundary was quoted.*No boundaries found in payload/s, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary="0000 "), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (quoted boundary value - whitespace between)", + note => q( + PHP 5.2.3 - uses "0000 " as boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500140" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500141" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500142" + ), + match_log => { + debug => [ qr/boundary was quoted/s, 1 ], + -debug => [ qr/No boundaries found in payload/s, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary="0000 1111"), + ], + normalize_raw_request_data( + q( + --0000 1111 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 1111 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 1111 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000 1111-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (quoted boundary value - contained quote)", + note => q( + PHP 5.2.3 - uses "0000 " as boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500143" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500144" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500145" + ), + match_log => { + debug => [ qr/Invalid boundary in C-T \(characters\)/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary="00\"00"), + ], + normalize_raw_request_data( + q( + --00"00 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --00"00 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --00"00 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --00"00-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (quoted boundary value - two quoted values)", + note => q( + PHP 5.2.3 - does not work, uses "00" as boundary. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500146" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500147" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500148" + ), + match_log => { + debug => [ qr/Invalid boundary in C-T \(characters\)/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary="00""00"), + ], + normalize_raw_request_data( + q( + --00"00 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --00"00 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --00"00 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --00"00-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (partial quoted boundary value - only start quote)", + note => q( + PHP 5.2.3 - breaks parsing. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500149" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500150" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500151" + ), + match_log => { + debug => [ qr/Invalid boundary in C-T \(quote\)/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary="0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (partial quoted boundary value - only end quote)", + note => q( + PHP 5.2.3 - breaks parsing. + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500152" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500153" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500154" + ), + match_log => { + debug => [ qr/Invalid boundary in C-T \(quote\)/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000"), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image"; filename="image.jpg" + Content-Type: image/jpeg + + BINARYDATA + --0000-- + ), + ), + ), +}, + +# Multipart mixed +{ + type => "misc", + comment => "multipart parser (multipart mixed - normal)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500155" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500156" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500157" + ), + match_log => { + debug => [ qr/Invalid Content-Disposition header/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: attachment + Content-Type: multipart/mixed; boundary=BbC04y + + --BbC04y + Content-Disposition: file; filename="file1.txt" + Content-Type: text/plain + + ... contents of file1.txt ... + --BbC04y + Content-Disposition: file; filename="file2.gif + Content-Type: image/jpeg + Content-Transfer-Encoding: binary + + ...contents of file2.gif... + --0000-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (multipart mixed - missing disposition)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,id:500158" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny,id:500159" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500160" + ), + match_log => { + debug => [ qr/Part missing Content-Disposition header/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Type: multipart/mixed; boundary=BbC04y + + --BbC04y + Content-Disposition: file; filename="file1.txt" + Content-Type: text/plain + + ... contents of file1.txt ... + --BbC04y + Content-Disposition: file; filename="file2.gif + Content-Type: image/jpeg + Content-Transfer-Encoding: binary + + ...contents of file2.gif... + --0000-- + ), + ), + ), +}, + +# File limits +{ + type => "misc", + comment => "multipart parser (normal)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecTmpDir "$ENV{TEMP_DIR}" + SecUploadDir "$ENV{UPLOAD_DIR}" + SecUploadKeepFiles On + SecUploadFileLimit 2 + + # These should be set + SecRule MULTIPART_STRICT_ERROR "!\@eq 1" "phase:2,deny,id:500161" + SecRule MULTIPART_FILE_LIMIT_EXCEEDED "!\@eq 1" "phase:2,deny,id:500162" + + # This should not be set + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,id:500163" + + # Theses should still be accurate + SecRule &FILES "!\@eq 3" "phase:2,deny,id:500164" + SecRule &FILES_NAMES "!\@eq 3" "phase:2,deny,id:500165" + SecRule &FILES_SIZES "!\@eq 3" "phase:2,deny,id:500166" + SecRule FILES_SIZES:/^image/ "\@eq 0" "phase:2,deny,id:500167" + + # This should be the SecUploadFileLimit + SecRule &FILES_TMPNAMES "!\@eq 2" "phase:2,deny,id:500168" + ), + match_log => { + debug => [ qr/Multipart: Upload file limit exceeded.*name: test.*variable: This is test data/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => q(multipart/form-data; boundary=0000), + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name" + + Brian Rectanus + --0000 + Content-Disposition: form-data; name="email" + + brian.rectanus@breach.com + --0000 + Content-Disposition: form-data; name="image1"; filename="image1.jpg" + Content-Type: image/jpeg + + BINARYDATA1 + --0000 + Content-Disposition: form-data; name="image2"; filename="image2.jpg" + Content-Type: image/jpeg + + BINARYDATA2 + --0000 + Content-Disposition: form-data; name="image3"; filename="image3.jpg" + Content-Type: image/jpeg + + BINARYDATA3 + --0000 + Content-Disposition: form-data; name="test" + + This is test data. + --0000-- + ), + ), + ), +}, + diff --git a/tests/regression/misc/00-phases.t b/tests/regression/misc/00-phases.t new file mode 100644 index 0000000..2f1c4e5 --- /dev/null +++ b/tests/regression/misc/00-phases.t @@ -0,0 +1,151 @@ +### Test the phases + +# Phase 1 (request headers) +{ + type => "misc", + comment => "phase 1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule REQUEST_LINE "^POST" "phase:1,pass,log,auditlog,id:500169" + SecRule ARGS "val1" "phase:1,pass,log,auditlog,id:500170" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:1,pass,log,auditlog,id:500171" + SecRule RESPONSE_BODY "TEST" "phase:1,pass,log,auditlog,id:500172" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE/, 1 ], + -error => [ qr/Pattern match .* (ARGS|RESPONSE)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# Phase 2 (request body) +{ + type => "misc", + comment => "phase 2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule REQUEST_LINE "^POST" "phase:2,pass,log,auditlog,id:500173" + SecRule ARGS "val1" "phase:2,pass,log,auditlog,id:500174" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:2,pass,log,auditlog,id:500175" + SecRule RESPONSE_BODY "TEST" "phase:2,pass,log,auditlog,id:500176" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE.*Pattern match "val1" at ARGS/s, 1 ], + -error => [ qr/Pattern match .* RESPONSE/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# Phase 3 (response headers) +{ + type => "misc", + comment => "phase 3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule REQUEST_LINE "^POST" "phase:3,pass,log,auditlog,id:500177" + SecRule ARGS "val1" "phase:3,pass,log,auditlog,id:500178" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:3,pass,log,auditlog,id:500179" + SecRule RESPONSE_BODY "TEST" "phase:3,pass,log,auditlog,id:500180" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE.*Pattern match "val1" at ARGS.*Pattern match "\." at RESPONSE_HEADERS/s, 1 ], + -error => [ qr/Pattern match .* RESPONSE_BODY/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# Phase 4 (response body) +{ + type => "misc", + comment => "phase 4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecRule REQUEST_LINE "^POST" "phase:4,pass,log,auditlog,id:500181" + SecRule ARGS "val1" "phase:4,pass,log,auditlog,id:500182" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:4,pass,log,auditlog,id:500183" + SecRule RESPONSE_BODY "TEST" "phase:4,pass,log,auditlog,id:500184" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE.*Pattern match "val1" at ARGS.*Pattern match "\." at RESPONSE_HEADERS.*Pattern match "TEST" at RESPONSE_BODY/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# Phase 5 (logging) +{ + type => "misc", + comment => "phase 5", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule REQUEST_LINE "^POST" "phase:5,pass,log,auditlog,id:500185" + SecRule ARGS "val1" "phase:5,pass,log,auditlog,id:500186" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:5,pass,log,auditlog,id:500187" + SecRule RESPONSE_BODY "TEST" "phase:5,pass,log,auditlog,id:500188" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE.*Pattern match "val1" at ARGS.*Pattern match "\." at RESPONSE_HEADERS.*Pattern match "TEST" at RESPONSE_BODY/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, diff --git a/tests/regression/misc/10-pcre.t b/tests/regression/misc/10-pcre.t new file mode 100644 index 0000000..61c0a30 --- /dev/null +++ b/tests/regression/misc/10-pcre.t @@ -0,0 +1,38 @@ +### PCRE Limits + +{ + type => "misc", + comment => "PCRE limits", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + + SecRequestBodyAccess On + SecRequestBodyLimit 1000000 + SecRequestBodyInMemoryLimit 1000000 + + # Set Limits low + SecPcreMatchLimit 100 + SecPcreMatchLimitRecursion 100 + + # Poor REGEX + SecRule ARGS "(?:(.{2,})\\1{32,})" "phase:2,deny,capture,msg:'REDoS',id:500053" + # Detect PCRE limits exceeded + SecRule TX:MSC_PCRE_LIMITS_EXCEEDED "!\@streq 0" "phase:2,deny,msg:'ModSecurity Internal Error Flagged: %{MATCHED_VAR_NAME}',id:500054" + ), + match_log => { + debug => [ qr/PCRE limits exceeded/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/index.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "test=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad", + ), +}, diff --git a/tests/regression/misc/20-status-engine.t b/tests/regression/misc/20-status-engine.t new file mode 100644 index 0000000..5fffe79 --- /dev/null +++ b/tests/regression/misc/20-status-engine.t @@ -0,0 +1,49 @@ +### Test the SecStatusEngine + +# On +{ + type => "misc", + comment => "Setting SecStatusEngine to On", + conf => qq( + SecRuleEngine On + SecStatusEngine On + ), + match_log => { + error => [ qr/ModSecurity: StatusEngine call successfully sent/, 1], + -error => [ qr/Status engine is currently disabled, enable it by set SecStatusEngine to On/, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, +# Off +{ + type => "misc", + comment => "Setting SecStatusEngine to Off", + conf => qq( + SecRuleEngine On + SecStatusEngine Off + ), + match_log => { + -error => [ qr/ModSecurity: StatusEngine call successfully sent/, 1], + error => [ qr/Status engine is currently disabled, enable it by set SecStatusEngine to On/, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + diff --git a/tests/regression/misc/25-libinjection.t b/tests/regression/misc/25-libinjection.t new file mode 100644 index 0000000..28b133f --- /dev/null +++ b/tests/regression/misc/25-libinjection.t @@ -0,0 +1,110 @@ +### libinjection. + +{ + type => "misc", + comment => "libinjection SQLi - with SQLi", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + + SecRule REQUEST_BODY "\@detectSQLi" "id:192372,log,deny" + ), + match_log => { + error => [ qr/detected SQLi using libinjection/, 1], + debug => [ qr/detected SQLi using libinjection/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/index.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=-1' and 1=1 union/* foo */select load_file('/etc/passwd')--" + ), +}, +{ + type => "misc", + comment => "libinjection SQLi - without SQLi", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + + SecRule REQUEST_BODY "\@detectSQLi" "id:192372,log,deny" + ), + match_log => { + -error => [ qr/detected SQLi using libinjection/, 1], + -debug => [ qr/detected SQLi using libinjection/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/index.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=hello cruel world" + ), +}, +{ + type => "misc", + comment => "libinjection XSS - with XSS", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + + SecRule REQUEST_BODY "\@detectXSS" "id:192372,log,deny" + ), + match_log => { + error => [ qr/detected XSS using libinjection/, 1], + debug => [ qr/detected XSS using libinjection/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/index.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=" + ), +}, +{ + type => "misc", + comment => "libinjection XSS - without XSS", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + + SecRule REQUEST_BODY "\@detectXSS" "id:192372,log,deny" + ), + match_log => { + -error => [ qr/detected XSS using libinjection/, 1], + -debug => [ qr/detected XSS using libinjection/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/index.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=hello cruel world" + ), +} diff --git a/tests/regression/misc/40-secRemoteRules.t.in b/tests/regression/misc/40-secRemoteRules.t.in new file mode 100644 index 0000000..93acd73 --- /dev/null +++ b/tests/regression/misc/40-secRemoteRules.t.in @@ -0,0 +1,43 @@ +### SecRemoteRules + +{ + type => "misc", + comment => "SecRemoteRules load", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRemoteRules 123 "https://www.modsecurity.org/modsecurity-regression-test-secremoterules.txt" + ), + match_log => { + error => [ qr/ModSecurity: Loaded 1 rule/, 1], + }, +}, +{ + type => "misc", + comment => "SecRemoteRules apply some remote rules", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRemoteRules 123 "https://www.modsecurity.org/modsecurity-regression-test-secremoterules.txt" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Matched phrase \"127.0.0.1\" at REQUEST_FILENAME./, 1], + debug => [ qr/Matched phrase \"127.0.0.1\" at REQUEST_FILENAME/, 1 ], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/127.0.0.1.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=-1' and 1=1 union/* foo */select load_file('/etc/passwd')--" + ), +}, + diff --git a/tests/regression/misc/50-ipmatchfromfile-external.t.in b/tests/regression/misc/50-ipmatchfromfile-external.t.in new file mode 100644 index 0000000..6b674ca --- /dev/null +++ b/tests/regression/misc/50-ipmatchfromfile-external.t.in @@ -0,0 +1,71 @@ +### ipMatchFromFile external resource + +{ + type => "misc", + comment => "ipMatchFromFile", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule REMOTE_ADDR "\@ipMatchFromFile https://www.modsecurity.org/modsecurity-regression-test.txt" "id:10500,pass" + ), + match_log => { + error => [ qr/ModSecurity: Warning. IPmatchFromFile: \"127.0.0.1\" matched at REMOTE_ADDR./, 1], + debug => [ qr/IPmatchFromFile: \"127.0.0.1\" matched at REMOTE_ADDR./, 1 ], + -error => [ qr/ModSecurity: Problems loading external resources:/, 1], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/127.0.0.1.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=-1' and 1=1 union/* foo */select load_file('/etc/passwd')--" + ), +}, +{ + type => "misc", + comment => "ipMatchFromFile - 404 download", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRemoteRulesFailAction Warn + SecRule REMOTE_ADDR "\@ipMatchFromFile https://www.modsecurity.org/modsecurity-regression-test-404.txt" "id:10500,pass" + ), + match_log => { + error => [ qr/ModSecurity: Problems loading external resources: Failed to download: \"https:\/\/www.modsecurity.org\/modsecurity-regression-test-404.txt\" error: HTTP response code said error./, 1], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/127.0.0.1.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=-1' and 1=1 union/* foo */select load_file('/etc/passwd')--" + ), +}, +{ + type => "misc", + comment => "ipMatchFromFile - bad certificate name", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRemoteRulesFailAction Warn + SecRule REMOTE_ADDR "\@ipMatchFromFile https://status.modsecurity.org/modsecurity-regression-test-huge-ip-list.txt" "id:10500,pass" + ), + match_log => { + error => [ qr/ModSecurity: Problems loading external resources: Failed to download: \"https:\/\/status.modsecurity.org\/modsecurity-regression-test-huge-ip-list.txt\" error: [SSL peer certificate or SSH remote key was not OK.|Couldn't connect to server.]/, 1], + }, +}, + diff --git a/tests/regression/misc/60-pmfromfile-external.t.in b/tests/regression/misc/60-pmfromfile-external.t.in new file mode 100644 index 0000000..2910ed1 --- /dev/null +++ b/tests/regression/misc/60-pmfromfile-external.t.in @@ -0,0 +1,84 @@ +### pmfromfile external resource + +{ + type => "misc", + comment => "pmfromfile", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule REQUEST_FILENAME "\@pmFromFile https://www.modsecurity.org/modsecurity-regression-test.txt" "id:'123',phase:2,log,pass,t:none" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Matched phrase \"127.0.0.1\" at REQUEST_FILENAME./, 1], + debug => [ qr/Matched phrase \"127.0.0.1\" at REQUEST_FILENAME/, 1 ], + -error => [ qr/ModSecurity: Problems loading external resources:/, 1], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/127.0.0.1.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=-1' and 1=1 union/* foo */select load_file('/etc/passwd')--" + ), +}, +{ + type => "misc", + comment => "pmfromfile - 404 download", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRemoteRulesFailAction Warn + SecRule REQUEST_FILENAME "\@pmFromFile https://www.modsecurity.org/modsecurity-regression-test-404.txt" "id:'123',phase:2,log,pass,t:none" + + ), + match_log => { + error => [ qr/ModSecurity: Problems loading external resources: Failed to download: \"https:\/\/www.modsecurity.org\/modsecurity-regression-test-404.txt\" error: HTTP response code said error./, 1], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/127.0.0.1.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=-1' and 1=1 union/* foo */select load_file('/etc/passwd')--" + ), +}, +{ + type => "misc", + comment => "pmfromfile - bad certificate name", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRemoteRulesFailAction Warn + SecRule REQUEST_FILENAME "\@pmFromFile https://status.modsecurity.org/modsecurity-regression-test.txt" "id:'123',phase:2,log,pass,t:none" + + ), + match_log => { + error => [ qr/ModSecurity: Problems loading external resources: Failed to download: \"https:\/\/status.modsecurity.org\/modsecurity-regression-test.txt\" error: [SSL peer certificate or SSH remote key was not OK.|Couldn't connect to server.]/, 1], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/127.0.0.1.html", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + # Args + "some_variable=-1' and 1=1 union/* foo */select load_file('/etc/passwd')--" + ), +}, + diff --git a/tests/regression/rule/00-basics.t b/tests/regression/rule/00-basics.t new file mode 100644 index 0000000..94fca34 --- /dev/null +++ b/tests/regression/rule/00-basics.t @@ -0,0 +1,91 @@ +### Tests for basic rule components + +# SecAction +{ + type => "rule", + comment => "SecAction (override default)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecAction "nolog,id:500001" + ), + match_log => { + -error => [ qr/500001/, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Warning\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecRule +{ + type => "rule", + comment => "SecRule (no action)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecDefaultAction "phase:2,deny,status:403" + SecRule ARGS:test "value" "id:500032" + ), + match_log => { + error => [ qr/500032/, 1 ], + debug => [ qr/Rule [0-9a-f]+: SecRule "ARGS:test" "\@rx value" "phase:2,deny,status:403,id:500032"$/m, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?test=value", + ), +}, +{ + type => "rule", + comment => "SecRule (action)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecDefaultAction "phase:2,pass" + SecRule ARGS:test "value" "deny,status:403,id:500033" + ), + match_log => { + error => [ qr/ModSecurity: /, 1 ], + debug => [ qr/Rule [0-9a-f]+: SecRule "ARGS:test" "\@rx value" "phase:2,deny,status:403,id:500033"$/m, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?test=value", + ), +}, +{ + type => "rule", + comment => "SecRule (chain)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecDefaultAction "phase:2,log,noauditlog,pass,tag:foo" + SecRule ARGS:test "value" "chain,phase:2,deny,status:403,id:500034" + SecRule &ARGS "\@eq 1" "chain," + SecRule REQUEST_METHOD "\@streq GET" + ), + match_log => { + error => [ qr/ModSecurity: /, 1 ], + debug => [ qr/Rule [0-9a-f]+: SecRule "ARGS:test" "\@rx value" "phase:2,log,noauditlog,tag:foo,chain,deny,status:403,id:500034"\r?\n.*Rule [0-9a-f]+:/ ] + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?test=value", + ), +}, diff --git a/tests/regression/rule/00-inheritance.t b/tests/regression/rule/00-inheritance.t new file mode 100644 index 0000000..c661c28 --- /dev/null +++ b/tests/regression/rule/00-inheritance.t @@ -0,0 +1,4 @@ +### Tests for rule inheritance + +### TODO: +# SecRuleInheritance diff --git a/tests/regression/rule/10-xml.t b/tests/regression/rule/10-xml.t new file mode 100644 index 0000000..ea9d6ad --- /dev/null +++ b/tests/regression/rule/10-xml.t @@ -0,0 +1,428 @@ +### Test for XML operator rules + +### Validate Scheme +# OK +{ + type => "rule", + comment => "validateSchema (validate ok)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500005, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500006 + SecRule XML "\@validateSchema $ENV{CONF_DIR}/SoapEnvelope.xsd" "id:500007, \\ + phase:3:,deny" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 1\).*Target value: "\[XML document tree\]".*Successfully validated payload against Schema/s, 1 ], + -debug => [ qr/XML parser error|validation failed|Failed to load/, 1 ], + -error => [ qr/XML parser error|validation failed|Failed to load/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + 12123 + + + + ), + ), + ), +}, +# Failed attribute value +{ + type => "rule", + comment => "validateSchema (validate attribute value failed)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500008, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500009 + SecRule XML "\@validateSchema $ENV{CONF_DIR}/SoapEnvelope.xsd" "id:500010 \\ + phase:2,deny,log,auditlog,id:12345" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 1\).*Target value: "\[XML document tree\]".*'badval' is not a valid value of the local atomic type.*Schema validation failed/s, 1 ], + -debug => [ qr/Successfully validated payload against Schema|\n\r?\n/, 1 ], + audit => [ qr/^Message: Element.*'badval' is not a valid value of the local atomic type\.\nMessage:/m, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + 12123 + + + + ), + ), + ), +}, +# Failed validation +{ + type => "rule", + comment => "validateSchema (validate failed)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500011, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500012 + SecRule XML "\@validateSchema $ENV{CONF_DIR}/SoapEnvelope.xsd" "id:500013 \\ + phase:2,deny,id:12345" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 1\).*Target value: "\[XML document tree\]".*element is not expected/s, 1 ], + -debug => [ qr/XML parser error|Failed to load/, 1 ], + -error => [ qr/XML parser error|Failed to load/, 1 ], + audit => [ qr/^Message: Element.*This element is not expected.*\nMessage:/m, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + 12123 + + + + ), + ), + ), +}, +# Bad XML +{ + type => "rule", + comment => "validateSchema (bad XML)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500014, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500015 + SecRule XML "\@validateSchema $ENV{CONF_DIR}/SoapEnvelope.xsd" "id:500016, \\ + phase:2,deny,id:12345" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 0\).*XML parser error.*validation failed because content is not well formed/s, 1 ], + -debug => [ qr/Failed to load|Successfully validated/, 1 ], + -error => [ qr/Failed to load|Successfully validated/, 1 ], + audit => [ qr/^Message: .*Failed parsing document.*\nMessage:/m, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + 12123 + + + + ), + ), + ), +}, +# Bad schema +{ + type => "rule", + comment => "validateSchema (bad schema)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500017 \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500018 + SecRule XML "\@validateSchema $ENV{CONF_DIR}/SoapEnvelope-bad.xsd" "id:500019 \\ + phase:2,deny,id:12345" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 1\).*Target value: "\[XML document tree\]".*Failed to parse the XML resource.*Failed to load Schema/s, 1 ], + audit => [ qr/^Message: .*Failed to parse the XML resource.*\nMessage: Rule processing failed/m, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + 12123 + + + + ), + ), + ), +}, + +# Validate DTD +# OK +{ + type => "rule", + comment => "validateDTD (validate ok)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500020, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500021 + SecRule XML "\@validateDTD $ENV{CONF_DIR}/SoapEnvelope.dtd" "id:500022, \\ + phase:2,deny,id:12345" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 1\).*Target value: "\[XML document tree\]".*Successfully validated payload against DTD/s, 1 ], + -debug => [ qr/XML parser error|validation failed|Failed to load/, 1 ], + -error => [ qr/XML parser error|validation failed|Failed to load/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + + 12123 + + + + ), + ), + ), +}, +# Failed validation +{ + type => "rule", + comment => "validateDTD (validate failed)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500023, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500024 + SecRule XML "\@validateDTD $ENV{CONF_DIR}/SoapEnvelope.dtd" "id:500025, \\ + phase:2,deny,id:12345" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 1\).*Target value: "\[XML document tree\]".*content does not follow the DTD/s, 1 ], + -debug => [ qr/XML parser error|Failed to load/, 1 ], + -error => [ qr/XML parser error|Failed to load/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + + 12123 + + + + ), + ), + ), +}, +# Bad XML +{ + type => "rule", + comment => "validateDTD (bad XML)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500026, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500027 + SecRule XML "\@validateDTD $ENV{CONF_DIR}/SoapEnvelope.dtd" "id:500028, \\ + phase:2,deny,id:12345" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 0\).*XML parser error.*validation failed because content is not well formed/s, 1 ], + -debug => [ qr/Failed to load|Successfully validated/, 1 ], + -error => [ qr/Failed to load|Successfully validated/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + + 12123 + + + + ), + ), + ), +}, +# Bad DTD +{ + type => "rule", + comment => "validateDTD (bad DTD)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecXmlExternalEntity On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500029, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500030 + SecRule XML "\@validateDTD $ENV{CONF_DIR}/SoapEnvelope-bad.dtd" "id:500031 \\ + phase:2,deny,id:12345" + ), + match_log => { + debug => [ qr/XML: Initialising parser.*XML: Parsing complete \(well_formed 1\).*Target value: "\[XML document tree\]".*Failed to load DTD/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q( + + + + + + 12123 + + + + ), + ), + ), +}, diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t new file mode 100644 index 0000000..e75f89c --- /dev/null +++ b/tests/regression/rule/15-json.t @@ -0,0 +1,39 @@ +### Test for JSON parser + +### +# OK +{ + type => "rule", + comment => "json parser", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "application/json" \\ + "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS:foo "bar" "id:'200441',phase:3,log" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Pattern match "bar" at ARGS:foo.|ModSecurity: JSON support was not enabled/s, 1 ], + debug => [ qr/Adding JSON argument 'foo' with value 'bar'|JSON support was not enabled/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q( + { + "foo":"bar", + "mod":"sec" + } + ), + ), + ), +} + diff --git a/tests/regression/rule/20-exceptions.t b/tests/regression/rule/20-exceptions.t new file mode 100644 index 0000000..71e3c39 --- /dev/null +++ b/tests/regression/rule/20-exceptions.t @@ -0,0 +1,129 @@ +### Tests for rule exceptions + +# SecRuleRemoveById +{ + type => "rule", + comment => "SecRuleRemoveById (single)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:101010" + SecRuleRemoveById 101010 + ), + match_log => { + -error => [ qr/101010/, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "rule", + comment => "SecRuleRemoveById (multiple)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:101010" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:202020" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:303030" + SecRuleRemoveById 101010 202020 303030 + ), + match_log => { + -error => [ qr/101010|202020|303030/, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "rule", + comment => "SecRuleRemoveById (range)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:101010" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:202020" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:303030" + SecRuleRemoveById 101010-303030 + ), + match_log => { + -error => [ qr/101010|202020|303030/, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "rule", + comment => "SecRuleRemoveById (multiple + range)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:101010" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:202020" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:303030" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:404040" + SecRuleRemoveById 101010 202020-404040 + ), + match_log => { + -error => [ qr/101010|202020|303030|404040/, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecRuleRemoveByMsg +{ + type => "rule", + comment => "SecRuleRemoveByMsg", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,msg:'testing rule',id:500001" + SecRuleRemoveByMsg "testing rule" + ), + match_log => { + -error => [ qr/500001/, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + + diff --git a/tests/regression/server_root/conf/SoapEnvelope-bad.dtd b/tests/regression/server_root/conf/SoapEnvelope-bad.dtd new file mode 100644 index 0000000..7d6c19f --- /dev/null +++ b/tests/regression/server_root/conf/SoapEnvelope-bad.dtd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/regression/server_root/conf/SoapEnvelope-bad.xsd b/tests/regression/server_root/conf/SoapEnvelope-bad.xsd new file mode 100644 index 0000000..2acfd1d --- /dev/null +++ b/tests/regression/server_root/conf/SoapEnvelope-bad.xsd @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prose in the spec does not specify that attributes are allowed on the Body element + + + + + + + + + + + + + + + + + + + + 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern described in SOAP specification + + + + + + + + + + + + + + + Fault reporting structure + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/regression/server_root/conf/SoapEnvelope.dtd b/tests/regression/server_root/conf/SoapEnvelope.dtd new file mode 100644 index 0000000..0ad4a8a --- /dev/null +++ b/tests/regression/server_root/conf/SoapEnvelope.dtd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/regression/server_root/conf/SoapEnvelope.xsd b/tests/regression/server_root/conf/SoapEnvelope.xsd new file mode 100644 index 0000000..2b4a8c0 --- /dev/null +++ b/tests/regression/server_root/conf/SoapEnvelope.xsd @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prose in the spec does not specify that attributes are allowed on the Body element + + + + + + + + + + + + + + + + + + + + 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern described in SOAP specification + + + + + + + + + + + + + + + Fault reporting structure + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/regression/server_root/conf/httpd.conf.in b/tests/regression/server_root/conf/httpd.conf.in new file mode 100644 index 0000000..4cd534e --- /dev/null +++ b/tests/regression/server_root/conf/httpd.conf.in @@ -0,0 +1,28 @@ +### Base configuration for starting Apache httpd + +# TODO: Need to have these configurable +LoadModule security3_module @MSC_BASE_DIR@/src/.libs/mod_security3.so + + +LoadModule unixd_module @APXS_LIBEXECDIR@/mod_unixd.so + +LoadModule access_compat_module @APXS_LIBEXECDIR@/mod_access_compat.so +LoadModule authn_core_module @APXS_LIBEXECDIR@/mod_authn_core.so +LoadModule authz_core_module @APXS_LIBEXECDIR@/mod_authz_core.so + +ServerName localhost +CoreDumpDirectory @MSC_REGRESSION_SERVERROOT_DIR@/tmp +LogLevel debug +ErrorLog @MSC_REGRESSION_LOGS_DIR@/error.log + + +# File locations +PidFile @MSC_REGRESSION_LOGS_DIR@/httpd.pid +ScoreBoardFile @MSC_REGRESSION_LOGS_DIR@/httpd.scoreboard + +DocumentRoot @MSC_REGRESSION_DOCROOT_DIR@ + + Options +Indexes +FollowSymLinks + + + diff --git a/tests/regression/server_root/conf/match.lua b/tests/regression/server_root/conf/match.lua new file mode 100644 index 0000000..fafd39b --- /dev/null +++ b/tests/regression/server_root/conf/match.lua @@ -0,0 +1,14 @@ +-- Test matching Lua Script to just print debug messages +function main() + m.log(1, "Test message."); + m.log(2, "Test message."); + m.log(3, "Test message."); + m.log(4, "Test message."); + m.log(5, "Test message."); + m.log(6, "Test message."); + m.log(7, "Test message."); + m.log(8, "Test message."); + m.log(9, "Test message."); + + return "Lua script matched."; +end diff --git a/tests/regression/server_root/conf/ssdeep.txt b/tests/regression/server_root/conf/ssdeep.txt new file mode 100644 index 0000000..7ba5dc5 --- /dev/null +++ b/tests/regression/server_root/conf/ssdeep.txt @@ -0,0 +1,4 @@ +ssdeep,1.1--blocksize:hash:hash,filename +96:MbQ1L0LDX8GPI8ov3D2D9zd6/gz2wZhFvV0O598La8Kqvfi0znNa8Xi5SM7XRWCK:KvL8Gg8rWIz2ZKqvfjzQ55RpRHjftQ++,"modsecurity.conf-recommended" +192:b8B5UQvywcMIJuavpde/Yyz/U/vF+vGCoCvrQr/dw:afcnrvp8zqUvGrzr6,"README_WINDOWS.TXT" +96:+qK8Z4gA165/hquKNMi68zuEyMM9qNB26x:+RG4z6c1LyZOB26x,"README.TXT" diff --git a/tests/regression/server_root/conf/test.lua b/tests/regression/server_root/conf/test.lua new file mode 100644 index 0000000..1cff076 --- /dev/null +++ b/tests/regression/server_root/conf/test.lua @@ -0,0 +1,14 @@ +-- Test Lua Script to just print debug messages +function main() + m.log(1, "Test message."); + m.log(2, "Test message."); + m.log(3, "Test message."); + m.log(4, "Test message."); + m.log(5, "Test message."); + m.log(6, "Test message."); + m.log(7, "Test message."); + m.log(8, "Test message."); + m.log(9, "Test message."); + + return nil; +end diff --git a/src/apache_http_modsecurity.slo b/tests/regression/server_root/data/.empty similarity index 100% rename from src/apache_http_modsecurity.slo rename to tests/regression/server_root/data/.empty diff --git a/tests/regression/server_root/data/ip.dir b/tests/regression/server_root/data/ip.dir new file mode 100644 index 0000000..e69de29 diff --git a/tests/regression/server_root/htdocs/8k.txt b/tests/regression/server_root/htdocs/8k.txt new file mode 100644 index 0000000..6d17cf9 Binary files /dev/null and b/tests/regression/server_root/htdocs/8k.txt differ diff --git a/tests/regression/server_root/htdocs/index.html b/tests/regression/server_root/htdocs/index.html new file mode 100644 index 0000000..16c51c1 --- /dev/null +++ b/tests/regression/server_root/htdocs/index.html @@ -0,0 +1 @@ +INDEX diff --git a/tests/regression/server_root/htdocs/test.pdf b/tests/regression/server_root/htdocs/test.pdf new file mode 100644 index 0000000..a6ec2d0 Binary files /dev/null and b/tests/regression/server_root/htdocs/test.pdf differ diff --git a/tests/regression/server_root/htdocs/test.txt b/tests/regression/server_root/htdocs/test.txt new file mode 100644 index 0000000..2a02d41 --- /dev/null +++ b/tests/regression/server_root/htdocs/test.txt @@ -0,0 +1 @@ +TEST diff --git a/tests/regression/server_root/htdocs/test2.txt b/tests/regression/server_root/htdocs/test2.txt new file mode 100644 index 0000000..55d8fa4 --- /dev/null +++ b/tests/regression/server_root/htdocs/test2.txt @@ -0,0 +1 @@ +TEST 2 diff --git a/tests/regression/server_root/logs/audit/.empty b/tests/regression/server_root/logs/audit/.empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/regression/server_root/logs/subdir/.empty b/tests/regression/server_root/logs/subdir/.empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/regression/server_root/tmp/.empty b/tests/regression/server_root/tmp/.empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/regression/server_root/upload/.empty b/tests/regression/server_root/upload/.empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/regression/target/00-targets.t b/tests/regression/target/00-targets.t new file mode 100644 index 0000000..d00b5a5 --- /dev/null +++ b/tests/regression/target/00-targets.t @@ -0,0 +1,685 @@ +### Test basic targets + +# ARGS +{ + type => "target", + comment => "ARGS (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS "val1" "phase:2,log,pass,id:500189" + SecRule ARGS "val2" "phase:2,log,pass,id:500190" + ), + match_log => { + error => [ qr/Pattern match "val1" at ARGS.*Pattern match "val2" at ARGS/s, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS "val1" "phase:2,log,pass,id:500191" + SecRule ARGS "val2" "phase:2,log,pass,id:500192" + ), + match_log => { + error => [ qr/Pattern match "val1" at ARGS.*Pattern match "val2" at ARGS/s, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_COMBINED_SIZE +{ + type => "target", + comment => "ARGS_COMBINED_SIZE (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule ARGS_COMBINED_SIZE "\@eq 16" "phase:2,log,pass,id:500193" + ), + match_log => { + error => [ qr/Operator EQ matched 16 at ARGS_COMBINED_SIZE\./s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_COMBINED_SIZE (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule ARGS_COMBINED_SIZE "\@eq 16" "phase:2,log,pass,id:500194" + ), + match_log => { + error => [ qr/Operator EQ matched 16 at ARGS_COMBINED_SIZE\./s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_NAMES +{ + type => "target", + comment => "ARGS_NAMES (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_NAMES "arg1" "phase:2,log,pass,id:500195" + SecRule ARGS_NAMES "arg2" "phase:2,log,pass,id:500196" + ), + match_log => { + error => [ qr/Pattern match "arg1" at ARGS.*Pattern match "arg2" at ARGS/s, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_NAMES (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_NAMES "arg1" "phase:2,log,pass,id:500197" + SecRule ARGS_NAMES "arg2" "phase:2,log,pass,id:500198" + ), + match_log => { + error => [ qr/Pattern match "arg1" at ARGS_NAMES.*Pattern match "arg2" at ARGS_NAMES/s, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_GET +{ + type => "target", + comment => "ARGS_GET (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_GET "val1" "phase:2,log,pass,id:500199" + SecRule ARGS_GET "val2" "phase:2,log,pass,id:500200" + ), + match_log => { + error => [ qr/Pattern match "val1" at ARGS_GET.*Pattern match "val2" at ARGS_GET/s, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_GET (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_GET "val1" "phase:2,log,pass,id:500201" + SecRule ARGS_GET "val2" "phase:2,log,pass,id:500202" + ), + match_log => { + -error => [ qr/Pattern match/, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_GET_NAMES +{ + type => "target", + comment => "ARGS_GET_NAMES (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_GET_NAMES "arg1" "phase:2,log,pass,id:500203" + SecRule ARGS_GET_NAMES "arg2" "phase:2,log,pass,id:500204" + ), + match_log => { + error => [ qr/Pattern match "arg1" at ARGS_GET.*Pattern match "arg2" at ARGS_GET/s, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_GET_NAMES (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_GET_NAMES "arg1" "phase:2,log,pass,id:500205" + SecRule ARGS_GET_NAMES "arg2" "phase:2,log,pass,id:500206" + ), + match_log => { + -error => [ qr/Pattern match/, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_POST +{ + type => "target", + comment => "ARGS_POST (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_POST "val1" "phase:2,log,pass,id:500207" + SecRule ARGS_POST "val2" "phase:2,log,pass,id:500208" + ), + match_log => { + -error => [ qr/Pattern match/, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=b", + ), +}, +{ + type => "target", + comment => "ARGS_POST (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_POST "val1" "phase:2,log,pass,id:500209" + SecRule ARGS_POST "val2" "phase:2,log,pass,id:500210" + ), + match_log => { + error => [ qr/Pattern match "val1" at ARGS_POST.*Pattern match "val2" at ARGS_POST/s, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_POST_NAMES +{ + type => "target", + comment => "ARGS_POST_NAMES (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_POST_NAMES "arg1" "phase:2,log,pass,id:500211" + SecRule ARGS_POST_NAMES "arg2" "phase:2,log,pass,id:500212" + ), + match_log => { + -error => [ qr/Pattern match/, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_POST_NAMES (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_POST_NAMES "arg1" "phase:2,log,pass,id:500213" + SecRule ARGS_POST_NAMES "arg2" "phase:2,log,pass,id:500214" + ), + match_log => { + error => [ qr/Pattern match "arg1" at ARGS_POST.*Pattern match "arg2" at ARGS_POST/s, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, +# FULL_REQUEST +{ + type => "target", + comment => "FULL_REQUEST (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule FULL_REQUEST "arg1" "phase:4,log,pass,id:500211" + SecRule FULL_REQUEST "arg2" "phase:4,log,pass,id:500212" + ), + match_log => { + error => [ qr/Pattern match "arg1" at FULL_REQUEST.*Pattern match "arg2" at FULL_REQUEST/s, 1 ], + debug => [ qr/against FULL_REQUEST.*Target value: "GET \/test.txt\?arg1=val1\&arg2=val2 HTTP\/1.1\\n\\nTE: deflate,gzip;q=0.3\\nConnection: TE, close\\nHost: localhost:[0-9]+\\nUser-Agent: ModSecurity Regression Tests\/1.2.3\\n\\n\\x00"/s, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "FULL_REQUEST (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule FULL_REQUEST "arg1" "phase:4,log,pass,id:500213" + SecRule FULL_REQUEST "arg2" "phase:4,log,pass,id:500214" + ), + match_log => { + error => [ qr/Pattern match "arg1" at FULL_REQUEST.*Pattern match "arg2" at FULL_REQUEST/s, 1 ], + debug => [ qr/against FULL_REQUEST.*Target value: "POST \/test.txt HTTP\/1.1\\n\\nTE: deflate,gzip;q=0.3\\nConnection: TE, close\\nHost: localhost:[0-9]+\\nUser-Agent: ModSecurity Regression Tests\/1.2.3\\nContent-Type: application\/x-www-form-urlencoded\\nContent-Length: 19\\n\\narg1=val1&arg2=val2\\x00"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, +# FULL_REQUEST_LENGTH +{ + type => "target", + comment => "FULL_REQUEST_LENGTH (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule FULL_REQUEST_LENGTH "\@eq 1" "phase:4,log,pass,id:500211" + SecRule FULL_REQUEST_LENGTH "\@eq 115" "phase:4,log,pass,id:500212" + ), + match_log => { + error => [ qr/Operator EQ matched 115 at FULL_REQUEST_LENGTH./s, 1 ], + debug => [ qr/against FULL_REQUEST_LENGTH.*Target value: "115"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "FULL_REQUEST_LENGTH (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule FULL_REQUEST_LENGTH "\@eq 1" "phase:4,log,pass,id:500213" + SecRule FULL_REQUEST_LENGTH "\@eq 201" "phase:4,log,pass,id:500214" + ), + match_log => { + error => [ qr/Operator EQ matched 201 at FULL_REQUEST_LENGTH./s, 1 ], + debug => [ qr/against FULL_REQUEST_LENGTH.*Target value: "201"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + + + +# AUTH_TYPE +#{ +# type => "target", +# comment => "AUTH_TYPE", +# conf => qq( +# = 2.2> +# +# LoadModule authn_file_module modules/mod_authn_file.so +# +# +## +## +## LoadModule auth_module modules/mod_auth.so +## +## +# +# AuthType Basic +# AuthName Test +# AuthUserFile "$ENV{CONF_DIR}/htpasswd" +# Require user nobody +# +# SecRuleEngine On +# SecRequestBodyAccess On +# SecResponseBodyAccess On +# SecResponseBodyMimeType null +## SecDebugLog $ENV{DEBUG_LOG} +## SecDebugLogLevel 9 +# SecRule REQUEST_HEADERS:Authorization "Basic (.*)" "phase:2,log,pass,capture,chain" +# SecRule TX:1 "nobody:test" "t:none,t:base64Decode,chain" +# SecRule AUTH_TYPE "Basic" +# ), +# match_log => { +# error => [ qr/Pattern match "Basic" at AUTH_TYPE/s, 1 ], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", +# [ +# "Authorization" => "Basic bm9ib2R5OnRlc3Q=" +# ], +# ), +#}, + +## ENH: We cannot include this test as we cannot distribute the database. +## Instead we should create a simple test DB of our own. +## GEO +#{ +# type => "target", +# comment => "GEO (ip)", +# conf => qq( +# SecRuleEngine On +# SecDebugLog $ENV{DEBUG_LOG} +# SecDebugLogLevel 9 +# SecGeoLookupDB GeoLiteCity.dat +# SecRule ARGS:ip "\@geoLookup" "phase:2,log,pass,t:none" +# SecRule GEO:COUNTRY_CODE "\@streq US" "phase:2,log,pass,t:none" +# SecRule GEO:COUNTRY_CODE3 "\@streq USA" "phase:2,log,pass,t:none" +# SecRule GEO:COUNTRY_NAME "\@streq United States" "phase:2,log,pass,t:none" +# # ENH: Not in this database? +# SecRule GEO:COUNTRY_CONTINENT "\@streq NA" "phase:2,log,pass,t:none" +# SecRule GEO:REGION "\@streq CA" "phase:2,log,pass,t:none" +# SecRule GEO:CITY "\@streq San Diego" "phase:2,log,pass,t:none" +# SecRule GEO:POSTAL_CODE "\@streq 92123" "phase:2,log,pass,t:none" +# SecRule GEO:LATITUDE "\@beginsWith 32.8" "phase:2,log,pass,t:none" +# SecRule GEO:LONGITUDE "\@beginsWith 117.1" "phase:2,log,pass,t:none" +# SecRule GEO:DMA_CODE "\@streq 825" "phase:2,log,pass,t:none" +# SecRule GEO:AREA_CODE "\@streq 858" "phase:2,log,pass,t:none" +# ), +# match_log => { +# debug => [ qr/Geo lookup for "216.75.21.122" succeeded.*match "US" at GEO:COUNTRY_CODE.*match "USA" at GEO:COUNTRY_CODE3.*match "United States" at GEO:COUNTRY_NAME.*match "NA" at GEO:COUNTRY_CONTINENT.*match "CA" at GEO:REGION.*match "San Diego" at GEO:CITY.*match "92123" at GEO:POSTAL_CODE.*match "32.8" at GEO:LATITUDE.*match "825" at GEO:DMA_CODE.*match "858" at GEO:AREA_CODE/si, 1 ], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?ip=216.75.21.122", +# ), +#}, +#{ +# type => "target", +# comment => "GEO (host)", +# conf => qq( +# SecRuleEngine On +# SecDebugLog $ENV{DEBUG_LOG} +# SecDebugLogLevel 9 +# SecGeoLookupDB GeoLiteCity.dat +# SecRule ARGS:host "\@geoLookup" "phase:2,log,pass,t:none" +# SecRule GEO:COUNTRY_CODE "\@streq US" "phase:2,log,pass,t:none" +# SecRule GEO:COUNTRY_CODE3 "\@streq USA" "phase:2,log,pass,t:none" +# SecRule GEO:COUNTRY_NAME "\@streq United States" "phase:2,log,pass,t:none" +# # ENH: Not in this database? +# SecRule GEO:COUNTRY_CONTINENT "\@streq NA" "phase:2,log,pass,t:none" +# SecRule GEO:REGION "\@streq CA" "phase:2,log,pass,t:none" +# SecRule GEO:CITY "\@streq San Diego" "phase:2,log,pass,t:none" +# SecRule GEO:POSTAL_CODE "\@streq 92123" "phase:2,log,pass,t:none" +# SecRule GEO:LATITUDE "\@beginsWith 32.8" "phase:2,log,pass,t:none" +# SecRule GEO:LONGITUDE "\@beginsWith 117.1" "phase:2,log,pass,t:none" +# SecRule GEO:DMA_CODE "\@streq 825" "phase:2,log,pass,t:none" +# SecRule GEO:AREA_CODE "\@streq 858" "phase:2,log,pass,t:none" +# ), +# match_log => { +# debug => [ qr/Using address "\d+\.\d+\.\d+\.\d+".*Geo lookup for "www\.modsecurity\.org" succeeded.*match "US" at GEO:COUNTRY_CODE.*match "USA" at GEO:COUNTRY_CODE3.*match "United States" at GEO:COUNTRY_NAME.*match "NA" at GEO:COUNTRY_CONTINENT.*match "CA" at GEO:REGION.*match "San Diego" at GEO:CITY.*match "92123" at GEO:POSTAL_CODE.*match "32.8" at GEO:LATITUDE.*match "825" at GEO:DMA_CODE.*match "858" at GEO:AREA_CODE/si, 1 ], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?host=www.modsecurity.org", +# ), +#}, +#{ +# type => "target", +# comment => "GEO (failed lookup)", +# conf => qq( +# SecRuleEngine On +# SecDebugLog $ENV{DEBUG_LOG} +# SecDebugLogLevel 9 +# SecGeoLookupDB GeoLiteCity.dat +# SecRule ARGS:ip "\@geoLookup" "phase:2,log,pass,t:none" +# SecRule \&GEO "\@eq 0" "phase:2,log,deny,status:403,t:none" +# SecRule ARGS:badip "\@geoLookup" "phase:2,log,pass,t:none" +# SecRule \&GEO "!\@eq 0" "phase:2,log,deny,status:403,t:none" +# ), +# match_log => { +# -debug => [ qr/Geo lookup for "127\.0\.0\.1" succeeded/si, 1 ], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?ip=216.75.21.122&badip=127.0.0.1", +# ), +#}, + +# TODO: ENV +# TODO: FILES +# TODO: FILES_COMBINED_SIZE +# TODO: FILES_NAMES +# TODO: FILES_SIZES +# TODO: FILES_TMPNAMES +# TODO: HIGHEST_SEVERITY +# TODO: MATCHED_VAR +# TODO: MATCHED_VAR_NAME +# TODO: MODSEC_BUILD +# TODO: MULTIPART_CRLF_LF_LINES +# TODO: MULTIPART_STRICT_ERROR +# TODO: MULTIPART_UNMATCHED_BOUNDARY +# TODO: PATH_INFO +# TODO: QUERY_STRING +# TODO: REMOTE_ADDR +# TODO: REMOTE_HOST +# TODO: REMOTE_PORT +# TODO: REMOTE_USER +# TODO: REQBODY_PROCESSOR +# TODO: REQBODY_PROCESSOR_ERROR +# TODO: REQBODY_PROCESSOR_ERROR_MSG +# TODO: REQUEST_BASENAME +# TODO: REQUEST_BODY +# TODO: REQUEST_COOKIES +# TODO: REQUEST_COOKIES_NAMES +# TODO: REQUEST_FILENAME +# TODO: REQUEST_HEADERS +# TODO: REQUEST_HEADERS_NAMES +# TODO: REQUEST_LINE +# TODO: REQUEST_METHOD +# TODO: REQUEST_PROTOCOL +# TODO: REQUEST_URI +# TODO: REQUEST_URI_RAW +# TODO: RESPONSE_BODY +# TODO: RESPONSE_CONTENT_LENGTH +# TODO: RESPONSE_CONTENT_TYPE +# TODO: RESPONSE_HEADERS +# TODO: RESPONSE_HEADERS_NAMES +# TODO: RESPONSE_PROTOCOL +# TODO: RESPONSE_STATUS +# TODO: RULE +# TODO: SCRIPT_BASENAME +# TODO: SCRIPT_FILENAME +# TODO: SCRIPT_GID +# TODO: SCRIPT_GROUPNAME +# TODO: SCRIPT_MODE +# TODO: SCRIPT_UID +# TODO: SCRIPT_USERNAME +# TODO: SERVER_ADDR +# TODO: SERVER_NAME +# TODO: SERVER_PORT +# TODO: SESSION +# TODO: SESSIONID +# TODO: TIME +# TODO: TIME_DAY +# TODO: TIME_EPOCH +# TODO: TIME_HOUR +# TODO: TIME_MIN +# TODO: TIME_MON +# TODO: TIME_SEC +# TODO: TIME_WDAY +# TODO: TIME_YEAR +# TODO: TX +# TODO: USERID +# TODO: WEBAPPID +# TODO: WEBSERVER_ERROR_LOG +# TODO: XML + diff --git a/tests/run-regression-tests.pl.in b/tests/run-regression-tests.pl.in new file mode 100755 index 0000000..4dfb413 --- /dev/null +++ b/tests/run-regression-tests.pl.in @@ -0,0 +1,811 @@ +#!@PERL@ +# +# Run regression tests. +# +# Syntax: run-regression-tests.pl [options] [file [N]] +# +# All: run-regression-tests.pl +# All in file: run-regression-tests.pl file +# Nth in file: run-regression-tests.pl file N +# +use strict; +use Time::HiRes qw(gettimeofday sleep); +use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG); +use File::Spec qw(rel2abs); +use File::Basename qw(basename dirname); +use FileHandle; +use IPC::Open2 qw(open2); +use IPC::Open3 qw(open3); +use Getopt::Std; +use Data::Dumper; +use IO::Socket; +use LWP::UserAgent; + +my @TYPES = qw(config misc action target rule); +my $SCRIPT = basename($0); +my $SCRIPT_DIR = File::Spec->rel2abs(dirname($0)); +my $REG_DIR = "$SCRIPT_DIR/regression"; +my $SROOT_DIR = "$REG_DIR/server_root"; +my $DATA_DIR = "$SROOT_DIR/data"; +my $TEMP_DIR = "$SROOT_DIR/tmp"; +my $UPLOAD_DIR = "$SROOT_DIR/upload"; +my $CONF_DIR = "$SROOT_DIR/conf"; +my $MODULES_DIR = q(@APXS_LIBEXECDIR@); +my $FILES_DIR = "$SROOT_DIR/logs"; +my $PID_FILE = "$FILES_DIR/httpd.pid"; +my $HTTPD = q(@APXS_HTTPD@); +my $PASSED = 0; +my $TOTAL = 0; +my $BUFSIZ = 32768; +my %C = (); +my %FILE = (); +my $UA_NAME = "ModSecurity Regression Tests/1.2.3"; +my $UA = LWP::UserAgent->new; +$UA->agent($UA_NAME); + +# Hack for testing the script w/o configure +if ($HTTPD eq "\@APXS_HTTPD\@") { + $HTTPD = "/usr/local/apache2/bin/httpd"; + $MODULES_DIR = "/usr/local/apache2/modules"; +} + +$SIG{TERM} = $SIG{INT} = \&handle_interrupt; + +my $platform = "apache_v3"; +my $platform_alternative = "apache"; + +my %opt; +getopts('A:E:D:C:T:H:a:p:dvh', \%opt); + +if ($opt{d}) { + $Data::Dumper::Indent = 1; + $Data::Dumper::Terse = 1; + $Data::Dumper::Pad = ""; + $Data::Dumper::Quotekeys = 0; +} + +sub usage { + print stderr <<"EOT"; +@_ +Usage: $SCRIPT [options] [file [N]] + + Options: + -A file Specify ModSecurity audit log to read. + -D file Specify ModSecurity debug log to read. + -E file Specify Apache httpd error log to read. + -C file Specify Apache httpd base conf file to generate/reload. + -H path Specify Apache httpd htdocs path. + -S path Specify Apache httpd server root path. + -a file Specify Apache httpd binary (default: httpd) + -p port Specify Apache httpd port (default: 8088) + -v Enable verbose output (details on failure). + -d Enable debugging output. + -h This help. + +EOT + + exit(1); +} + +usage() if ($opt{h}); + +### Check httpd binary +if (defined $opt{a}) { + $HTTPD = $opt{a}; +} +else { + $opt{a} = $HTTPD; +} +usage("Invalid Apache startup script: $HTTPD\n") unless (-e $HTTPD); + +### Defaults +$opt{A} = "$FILES_DIR/modsec_audit.log" unless (defined $opt{A}); +$opt{D} = "$FILES_DIR/modsec_debug.log" unless (defined $opt{D}); +$opt{E} = "$FILES_DIR/error.log" unless (defined $opt{E}); +$opt{C} = "$CONF_DIR/httpd.conf" unless (defined $opt{C}); +$opt{H} = "$SROOT_DIR/htdocs" unless (defined $opt{H}); +$opt{p} = 8088 unless (defined $opt{p}); +$opt{v} = 1 if ($opt{d}); + +unless (defined $opt{S}) { + my $httpd_root = `$HTTPD -V`; + ($opt{S} = $httpd_root) =~ s/.*-D HTTPD_ROOT="([^"]*)".*/$1/sm; +} + +%ENV = ( + %ENV, + SERVER_ROOT => $opt{S}, + SERVER_PORT => $opt{p}, + SERVER_NAME => "localhost", + TEST_SERVER_ROOT => $SROOT_DIR, + DATA_DIR => $DATA_DIR, + TEMP_DIR => $TEMP_DIR, + UPLOAD_DIR => $UPLOAD_DIR, + CONF_DIR => $CONF_DIR, + MODULES_DIR => $MODULES_DIR, + LOGS_DIR => $FILES_DIR, + SCRIPT_DIR => $SCRIPT_DIR, + REGRESSION_DIR => $REG_DIR, + DIST_ROOT => File::Spec->rel2abs(dirname("$SCRIPT_DIR/../../..")), + AUDIT_LOG => $opt{A}, + DEBUG_LOG => $opt{D}, + ERROR_LOG => $opt{E}, + HTTPD_CONF => $opt{C}, + HTDOCS => $opt{H}, + USER_AGENT => $UA_NAME, +); + +#dbg("OPTIONS: ", \%opt); + +if (-e "$PID_FILE") { + msg("Shutting down previous instance: $PID_FILE"); + httpd_stop(); +} + +if (defined $ARGV[0]) { + runfile(dirname($ARGV[0]), basename($ARGV[0]), $ARGV[1]); + done(); +} + +for my $type (@TYPES) { + my $dir = "$SCRIPT_DIR/regression/$type"; + my @cfg = (); + + # Get test names + opendir(DIR, "$dir") or quit(1, "Failed to open \"$dir\": $!"); + @cfg = grep { /\.t$/ && -f "$dir/$_" } readdir(DIR); + closedir(DIR); + + for my $cfg (sort @cfg) { + runfile($dir, $cfg); + } +} +done(); + + +sub runfile { + my($dir, $cfg, $testnum) = @_; + my $fn = "$dir/$cfg"; + my @data = (); + my $edata; + my @C = (); + my @test = (); + my $teststr; + my $n = 0; + my $pass = 0; + + open(CFG, "<$fn") or quit(1, "Failed to open \"$fn\": $!"); + @data = ; + + $edata = q/@C = (/ . join("", @data) . q/)/; + eval $edata; + quit(1, "Failed to read test data \"$cfg\": $@") if ($@); + + unless (@C) { + msg("\nNo tests defined for $fn"); + return; + } + + msg("\nLoaded ".@C." tests from $fn"); + for my $t (@C) { + $n++; + next if (defined $testnum and $n != $testnum); + + my $httpd_up = 0; + my %t = %{$t || {}}; + my $id = sprintf("%3d", $n); + my $out = ""; + my $rc = 0; + my $conf_fn; + + # Startup httpd with optionally included conf. + if (exists $t{conf} and defined $t{conf}) { + $conf_fn = sprintf "%s/%s_%s_%06d.conf", + $CONF_DIR, $t{type}, $cfg, $n; + #dbg("Writing test config to: $conf_fn"); + open(CONF, ">$conf_fn") or die "Failed to open conf \"$conf_fn\": $!\n"; + print CONF (ref $t{conf} eq "CODE" ? eval { &{$t{conf}} } : $t{conf}); + msg("$@") if ($@); + close CONF; + $httpd_up = httpd_start(\%t, "modsecurity_rules_file $conf_fn") ? 0 : 1; + } + else { + $httpd_up = httpd_start(\%t) ? 0 : 1; + } + + # Run any prerun setup + if ($rc == 0 and exists $t{prerun} and defined $t{prerun}) { + vrb("Executing perl prerun..."); + $rc = &{$t{prerun}}; + vrb("Perl prerun returned: $rc"); + } + + if ($httpd_up) { + # Perform the request and check response + if (exists $t{request}) { + my $resp = do_request($t{request}); + if (!$resp) { + msg("invalid response"); + vrb("RESPONSE: ", $resp); + $rc = 1; + } + else { + for my $key (keys %{ $t{match_response} || {}}) { + my($neg,$mtype) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_response}{$key}; + if (ref($m) eq "HASH") { + if ($m->{$platform}) { + $m = $m->{$platform}; + } + elsif ($m->{$platform_alternative}) { + $m = $m->{$platform_alternative}; + } + else { + my $ap = join(", ", keys %{$m}); + msg("Warning: trying to match: $mtype. Nothing " . + "to match in current platform: $platform. " . + "This test only contains cotent for: $ap."); + last; + } + } + my $match = match_response($mtype, $resp, $m); + if ($neg and defined $match) { + $rc = 1; + msg("response $mtype matched: $m"); + vrb($resp); + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("response $mtype failed to match: $m"); + vrb($resp); + last; + } + } + } + } + + # Run any arbitrary perl tests + if ($rc == 0 and exists $t{test} and defined $t{test}) { + dbg("Executing perl test(s)..."); + $rc = eval { &{$t{test}} }; + if (! defined $rc) { + msg("Error running test: $@"); + $rc = -1; + } + dbg("Perl tests returned: $rc"); + } + + # Search for all log matches + if ($rc == 0 and exists $t{match_log} and defined $t{match_log}) { + for my $key (keys %{ $t{match_log} || {}}) { + my($neg,$mtype) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_log}{$key}; + if (ref($m) eq "HASH") { + if ($m->{$platform}) { + $m = $m->{$platform}; + } + else { + my $ap = join(", ", keys %{$m}); + msg("Warning: trying to match: $mtype. Nothing " . + "to match in current platform: $platform. " . + "This test only contains cotent for: $ap."); + last; + } + } + my $match = match_log($mtype, @{$m || []}); + if ($neg and defined $match) { + $rc = 1; + msg("$mtype log matched: $m->[0]"); + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("$mtype log failed to match: $m->[0]"); + last; + } + } + } + + # Search for all file matches + if ($rc == 0 and exists $t{match_file} and defined $t{match_file}) { + sleep 1; # Make sure the file exists + for my $key (keys %{ $t{match_file} || {}}) { + my($neg,$fn) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_file}{$key}; + my $match = match_file($fn, $m); + if ($neg and defined $match) { + $rc = 1; + msg("$fn file matched: $m"); + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("$fn file failed match: $m"); + last; + } + } + } + } + else { + msg("Failed to start httpd."); + $rc = 1; + } + + if ($rc == 0) { + $pass++; + } + else { + vrb("Test Config: $conf_fn"); + vrb("Debug Log: $FILE{debug}{fn}"); + dbg(escape("$FILE{debug}{buf}")); + vrb("Error Log: $FILE{error}{fn}"); + dbg(escape("$FILE{error}{buf}")); + } + + msg(sprintf("%s) %s%s: %s%s", $id, $t{type}, (exists($t{comment}) ? " - $t{comment}" : ""), ($rc ? "failed" : "passed"), ((defined($out) && $out ne "")? " ($out)" : ""))); + + if ($httpd_up) { + $httpd_up = httpd_stop(\%t) ? 0 : 1; + } + + } + + $TOTAL += $testnum ? 1 : $n; + $PASSED += $pass; + + msg(sprintf("Passed: %2d; Failed: %2d", $pass, $testnum ? (1 - $pass) : ($n - $pass))); +} + +# Take out any indenting and translate LF -> CRLF +sub normalize_raw_request_data { + my $r = $_[0]; + + # Allow for indenting in test file + $r =~ s/^[ \t]*\x0d?\x0a//s; + my($indention) = ($r =~ m/^([ \t]*)/s); # indention taken from first line + $r =~ s/^$indention//mg; + $r =~ s/(\x0d?\x0a)[ \t]+$/$1/s; + + # Translate LF to CRLF + $r =~ s/^\x0a/\x0d\x0a/mg; + $r =~ s/([^\x0d])\x0a/$1\x0d\x0a/mg; + + return $r; +} + +sub do_raw_request { + my $sock = new IO::Socket::INET( + Proto => "tcp", + PeerAddr => "localhost", + PeerPort => $opt{p}, + ) or msg("Failed to connect to localhost:$opt{p}: $@"); + return unless ($sock); + + # Join togeather the request + my $r = join("", @_); + dbg($r); + + # Write to socket + print $sock "$r"; + $sock->shutdown(1); + + # Read from socket + my @resp = <$sock>; + $sock->close(); + + return HTTP::Response->parse(join("", @resp)); +} + +sub do_request { + my $r = $_[0]; + + # Allow test to execute code + if (ref $r eq "CODE") { + $r = eval { &$r }; + msg("$@") unless (defined $r); + } + + if (ref $r eq "HTTP::Request") { + my $resp = $UA->request($r); + dbg($resp->request()->as_string()) if ($opt{d}); + return $resp + } + else { + return do_raw_request($r); + } + + return; +} + + +sub match_response { + my($name, $resp, $re) = @_; + + msg("Warning: Empty regular expression.") if (!defined $re or $re eq ""); + + if ($name eq "status") { + return $& if ($resp->code =~ m/$re/); + } + elsif ($name eq "content") { + return $& if ($resp->content =~ m/$re/m); + } + elsif ($name eq "raw") { + return $& if ($resp->as_string =~ m/$re/m); + } + + return; +} + +sub read_log { + my($name, $timeout, $graph) = @_; + return match_log($name, undef, $timeout, $graph); +} + +sub match_log { + my($name, $re, $timeout, $graph) = @_; + my $t0 = gettimeofday; + my($fh,$rbuf) = ($FILE{$name}{fd}, \$FILE{$name}{buf}); + my $n = length($$rbuf); + my $rc = undef; + + unless (defined $fh) { + msg("Error: File \"$name\" is not opened for matching."); + return; + } + + $timeout = 0 unless (defined $timeout); + + my $i = 0; + my $graphed = 0; + READ: { + do { + my $nbytes = $fh->sysread($$rbuf, $BUFSIZ, $n); + if (!defined($nbytes)) { + msg("Error: Could not read \"$name\" log: $!"); + last; + } + elsif (!defined($re) and $nbytes == 0) { + last; + } + + # Remove APR pool debugging + $$rbuf =~ s/POOL DEBUG:[^\n]+PALLOC[^\n]+\n//sg; + + $n = length($$rbuf); + + #dbg("Match \"$re\" in $name \"$$rbuf\" ($n)"); + if ($$rbuf =~ m/$re/m) { + $rc = $&; + last; + } + # TODO: Use select()/poll() + sleep 0.1 unless ($nbytes == $BUFSIZ); + if ($graph and $opt{d}) { + $i++; + if ($i == 10) { + $graphed++; + $i=0; + print STDERR $graph if ($graphed == 1); + print STDERR "." + } + } + } while (gettimeofday - $t0 < $timeout); + } + print STDERR "\n" if ($graphed); + + return $rc; +} + +sub match_file { + my($neg,$fn) = ($_[0] =~ m/^(-?)(.*)$/); + unless (exists $FILE{$fn}) { + eval { + $FILE{$fn}{fn} = $fn; + $FILE{$fn}{fd} = new FileHandle($fn, O_RDONLY) or die "$!\n"; + $FILE{$fn}{fd}->blocking(0); + $FILE{$fn}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$fn\": $@"); + return; + } + } + return match_log($_[0], $_[1]); # timeout makes no sense +} + +sub quote_shell { + my($s) = @_; + return $s unless ($s =~ m|[^\w!%+,\-./:@^]|); + $s =~ s/(['\\])/\\$1/g; + return "'$s'"; +} + +sub escape { + my @new = (); + for my $c (split(//, $_[0])) { + my $oc = ord($c); + push @new, ((($oc >= 0x20 and $oc <= 0x7e) or $oc == 0x0a or $oc == 0x0d) ? $c : sprintf("\\x%02x", ord($c))); + } + join('', @new); +} + +sub dbg { + return unless(@_ and $opt{d}); + my $out = join "", map { + (ref $_ ne "" ? Dumper($_) : $_) + } @_; + $out =~ s/^/DBG: /mg; + print STDOUT "$out\n"; +} + +sub vrb { + return unless(@_ and $opt{v}); + msg(@_); +} + +sub msg { + return unless(@_); + my $out = join "", map { + (ref $_ ne "" ? Dumper($_) : $_) + } @_; + print STDOUT "$out\n"; +} + +sub handle_interrupt { + $SIG{TERM} = $SIG{INT} = \&handle_interrupt; + + msg("Interrupted via SIG$_[0]. Shutting down tests..."); + httpd_stop(); + + quit(1); +} + +sub quit { + my($ec,$msg) = @_; + $ec = 0 unless (defined $_[0]); + + msg("$msg") if (defined $msg); + + exit $ec; +} + +sub done { + if ($PASSED != $TOTAL) { + quit(1, "\n$PASSED/$TOTAL tests passed."); + } + + quit(0, "\nAll tests passed ($TOTAL)."); +} + +sub httpd_start { + my $t = shift; + httpd_reset_fd($t); + my @p = ( + $HTTPD, + -d => $opt{S}, + -f => $opt{C}, + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), + -k => "start", + ); + + my $httpd_out; + my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); + my $out = join("\\n", grep(!/POOL DEBUG/, (<$httpd_out>))); + close $httpd_out; + waitpid($httpd_pid, 0); + + my $rc = $?; + if ( WIFEXITED($rc) ) { + $rc = WEXITSTATUS($rc); + vrb("Httpd start returned with $rc.") if ($rc); + } + elsif( WIFSIGNALED($rc) ) { + msg("Httpd start failed with signal " . WTERMSIG($rc) . "."); + $rc = -1; + } + else { + msg("Httpd start failed with unknown error."); + $rc = -1; + } + + if (defined $out and $out ne "") { + vrb(join(" ", map { quote_shell($_) } @p)); + msg("Httpd start failed with error messages:\n$out"); + httpd_stop(); + return -1 + } + + # Look for startup msg + unless (defined match_log("error", qr/resuming normal operations/, 60, "Waiting on httpd to start: ")) { + vrb(join(" ", map { quote_shell($_) } @p)); + vrb(match_log("error", qr/(^.*ModSecurity: .*)/sm, 10)); + msg("Httpd server failed to start."); + httpd_stop(); + return -1; + } + + return $rc; +} + +sub httpd_stop { + my $t = shift; + my @p = ( + $HTTPD, + -d => $opt{S}, + -f => $opt{C}, + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), + -k => "stop", + ); + + my $httpd_out; + my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); + my $out = join("\\n", grep(!/POOL DEBUG/, (<$httpd_out>))); + close $httpd_out; + waitpid($httpd_pid, 0); + + if (defined $out and $out ne "") { + msg("Httpd stop failed with error messages:\n$out"); + return -1 + } + + my $rc = $?; + if ( WIFEXITED($rc) ) { + $rc = WEXITSTATUS($rc); + vrb("Httpd stop returned with $rc.") if ($rc); + } + elsif( WIFSIGNALED($rc) ) { + msg("Httpd stop failed with signal " . WTERMSIG($rc) . "."); + $rc = -1; + } + else { + msg("Httpd stop failed with unknown error."); + $rc = -1; + } + + # Look for startup msg + unless (defined match_log("error", qr/caught SIG[A-Z]+, shutting down/, 60, "Waiting on httpd to stop: ")) { + vrb(join(" ", map { quote_shell($_) } @p)); + msg("Httpd server failed to shutdown."); + sleep 0.5; + return -1; + } + + sleep 0.5; + + return $rc; +} + +sub httpd_reload { + my $t = shift; + httpd_reset_fd($t); + my @p = ( + $HTTPD, + -d => $opt{S}, + -f => $opt{C}, + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), + -k => "graceful", + ); + + my $httpd_out; + my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); + my $out = join("\\n", grep(!/POOL DEBUG/, (<$httpd_out>))); + close $httpd_out; + waitpid($httpd_pid, 0); + + if (defined $out and $out ne "") { + msg("Httpd reload failed with error messages:\n$out"); + return -1 + } + + my $rc = $?; + if ( WIFEXITED($rc) ) { + $rc = WEXITSTATUS($rc); + vrb("Httpd reload returned with $rc.") if ($rc); + } + elsif( WIFSIGNALED($rc) ) { + msg("Httpd reload failed with signal " . WTERMSIG($rc) . "."); + $rc = -1; + } + else { + msg("Httpd reload failed with unknown error."); + $rc = -1; + } + + # Look for startup msg + unless (defined match_log("error", qr/resuming normal operations/, 60, "Waiting on httpd to restart: ")) { + vrb(join(" ", map { quote_shell($_) } @p)); + msg("Httpd server failed to reload."); + return -1; + } + + return $rc; +} + +sub httpd_reset_fd { + my($t) = @_; + + # Cleanup + for my $key (keys %FILE) { + if (exists $FILE{$key}{fd} and defined $FILE{$key}{fd}) { + $FILE{$key}{fd}->close(); + } + delete $FILE{$key}; + } + + # Error + eval { + $FILE{error}{fn} = $opt{E}; + $FILE{error}{fd} = new FileHandle($opt{E}, O_RDWR|O_CREAT) or die "$!\n"; + $FILE{error}{fd}->blocking(0); + $FILE{error}{fd}->sysseek(0, 2); + $FILE{error}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$opt{E}\": $@"); + return undef; + } + + # Audit + eval { + $FILE{audit}{fn} = $opt{A}; + $FILE{audit}{fd} = new FileHandle($opt{A}, O_RDWR|O_CREAT) or die "$!\n"; + $FILE{audit}{fd}->blocking(0); + $FILE{audit}{fd}->sysseek(0, 2); + $FILE{audit}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$opt{A}\": $@"); + return undef; + } + + # Debug + eval { + $FILE{debug}{fn} = $opt{D}; + $FILE{debug}{fd} = new FileHandle($opt{D}, O_RDWR|O_CREAT) or die "$!\n"; + $FILE{debug}{fd}->blocking(0); + $FILE{debug}{fd}->sysseek(0, 2); + $FILE{debug}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$opt{D}\": $@"); + return undef; + } + + # Any extras listed in "match_log" + if ($t and exists $t->{match_log}) { + for my $k (keys %{ $t->{match_log} || {} }) { + my($neg,$fn) = ($k =~ m/^(-?)(.*)$/); + next if (!$fn or exists $FILE{$fn}); + eval { + $FILE{$fn}{fn} = $fn; + $FILE{$fn}{fd} = new FileHandle($fn, O_RDWR|O_CREAT) or die "$!\n"; + $FILE{$fn}{fd}->blocking(0); + $FILE{$fn}{fd}->sysseek(0, 2); + $FILE{$fn}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$fn\": $@"); + return undef; + } + } + } +} + +sub encode_chunked { + my($data, $size) = @_; + $size = 128 unless ($size); + my $chunked = ""; + + my $n = 0; + my $bytes = length($data); + while ($bytes >= $size) { + $chunked .= sprintf "%x\x0d\x0a%s\x0d\x0a", $size, substr($data, $n, $size); + $n += $size; + $bytes -= $size; + } + if ($bytes) { + $chunked .= sprintf "%x\x0d\x0a%s\x0d\x0a", $bytes, substr($data, $n, $bytes); + } + $chunked .= "0\x0d\x0a\x0d\x0a" +}