diff --git a/.travis.yml b/.travis.yml index ba3bb96..80ca023 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,12 +24,12 @@ addons: env: - VER_APACHE=2.2.34 - - VER_APACHE=2.4.27 + - 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 @@ -54,7 +54,7 @@ before_script: - 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 + - ./configure --with-apxs=/usr/local/apache2/bin/apxs --with-libmodsecurity=/usr/local/modsecurity - make - sudo make install - pwd 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 e7e6b2f..980cd02 100644 --- a/Makefile.am +++ b/Makefile.am @@ -15,13 +15,21 @@ 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/*.slo \ + src/*.o \ + src/*.so \ + src/.libs/*.so + CLEANDIRECTORIES = \ - t/logs + t/logs \ + src/.libs all: @@ -29,6 +37,13 @@ all: build/apxs-wrapper test: - cd t/ && ./TEST + 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/ax_prog_apache.m4 b/build/ax_prog_apache.m4 index b1dd5c6..aad2f2d 100644 --- a/build/ax_prog_apache.m4 +++ b/build/ax_prog_apache.m4 @@ -114,6 +114,9 @@ AC_DEFUN([AX_PROG_APACHE], if test -z "$APACHE" ; then 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 AC_MSG_ERROR("apache server executable not found"); @@ -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/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/mod_security3.c b/src/mod_security3.c index c347252..76b3fb7 100644 --- a/src/mod_security3.c +++ b/src/mod_security3.c @@ -1,4 +1,6 @@ +#include + #include "mod_security3.h" #include "msc_utils.h" #include "msc_config.h" @@ -132,6 +134,8 @@ static void store_tx_context(msc_t *msr, request_rec *r) 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); @@ -141,7 +145,14 @@ static msc_t *create_tx_context(request_rec *r) { } msr->r = r; - msr->t = msc_new_transaction(msc_apache->modsec, (Rules *)z->rules_set, (void *)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); @@ -387,6 +398,8 @@ static int hook_request_late(request_rec *r) return it; } #endif + + msc_process_request_body(msr->t); it = process_intervention(msr->t, r); if (it != N_INTERVENTION_STATUS) @@ -414,6 +427,7 @@ static int hook_log_transaction(request_rec *r) 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) @@ -463,8 +477,9 @@ static int process_request_headers(request_rec *r, msc_t *msr) { /* process uri */ { int it; - msc_process_uri(msr->t, r->unparsed_uri, r->method, r->protocol); + 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) { diff --git a/src/mod_security3.h b/src/mod_security3.h index 258d4eb..b1e9b28 100644 --- a/src/mod_security3.h +++ b/src/mod_security3.h @@ -3,7 +3,17 @@ #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" @@ -46,7 +56,7 @@ typedef struct typedef struct { - Rules *rules_set; + void *rules_set; int msc_state; char *name_for_debug; } msc_conf_t; diff --git a/src/msc_filters.c b/src/msc_filters.c index 8e274c9..3a18e21 100644 --- a/src/msc_filters.c +++ b/src/msc_filters.c @@ -62,6 +62,9 @@ apr_status_t input_filter(ap_filter_t *f, apr_bucket_brigade *pbbOut, 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); @@ -108,7 +111,7 @@ apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) msc_add_response_header(msr->t, key, val); } - msc_process_response_headers(msr->t, 200, "HTTP 1.1"); + msc_process_response_headers(msr->t, r->status, "HTTP 1.1"); it = process_intervention(msr->t, r); if (it != N_INTERVENTION_STATUS) diff --git a/t/conf/extra.conf.in b/t/conf/extra.conf.in index cb68896..6518559 100644 --- a/t/conf/extra.conf.in +++ b/t/conf/extra.conf.in @@ -2,6 +2,8 @@ CoreDumpDirectory /tmp/ +Include @ServerRoot@/.././t/conf/modules.conf + LoadModule security3_module "@ServerRoot@/.././src/.libs/mod_security3.so" @@ -18,6 +20,7 @@ modsecurity_rules 'SecDebugLogLevel 9' + modsecurity_rules 'SecRequestBodyAccess On' modsecurity_rules 'SecRule ARGS "evil" "phase:2,id:112,log,status:403,block,deny"' @@ -26,6 +29,7 @@ modsecurity_rules 'SecDebugLogLevel 9' + modsecurity_rules 'SecResponseBodyAccess On' modsecurity_rules 'SecRule ARGS "evil" "phase:4,id:114,log,status:403,block,deny"' @@ -40,6 +44,7 @@ modsecurity_rules 'SecDebugLogLevel 9' + modsecurity_rules 'SecRequestBodyAccess On' modsecurity_rules 'SecRule ARGS "evil" "phase:2,id:112,log,status:402,block,deny"' @@ -48,6 +53,7 @@ modsecurity_rules 'SecDebugLogLevel 9' + modsecurity_rules 'SecResponseBodyAccess On' modsecurity_rules 'SecRule ARGS "evil" "phase:4,id:1134,log,status:402,block,deny"' 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/tests/regression/server_root/data/.empty b/tests/regression/server_root/data/.empty new file mode 100644 index 0000000..e69de29 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" +}