From 813127aa1326197fe4e39319fafa4a56614eb58b Mon Sep 17 00:00:00 2001 From: brectanus Date: Thu, 22 May 2008 18:44:22 +0000 Subject: [PATCH] Added some basic regression tests. --- apache2/t/regression/config/00-load-modsec.t | 23 + .../regression/config/10-request-directives.t | 144 ++++++ .../t/regression/rule/00-disruptive-actions.t | 427 ++++++++++++++++++ apache2/t/run-regression-tests.pl.in | 81 +++- 4 files changed, 657 insertions(+), 18 deletions(-) create mode 100644 apache2/t/regression/config/00-load-modsec.t create mode 100644 apache2/t/regression/config/10-request-directives.t create mode 100644 apache2/t/regression/rule/00-disruptive-actions.t diff --git a/apache2/t/regression/config/00-load-modsec.t b/apache2/t/regression/config/00-load-modsec.t new file mode 100644 index 00000000..9c6ecc5f --- /dev/null +++ b/apache2/t/regression/config/00-load-modsec.t @@ -0,0 +1,23 @@ +{ + type => "config", + comment => "module loaded", + match_log => { + error => [ qr/ModSecurity for Apache.* 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 => [ qr/ModSecurity for Apache.* configured\./, 10 ], + }, +}, diff --git a/apache2/t/regression/config/10-request-directives.t b/apache2/t/regression/config/10-request-directives.t new file mode 100644 index 00000000..ac27f371 --- /dev/null +++ b/apache2/t/regression/config/10-request-directives.t @@ -0,0 +1,144 @@ + +### SecArgumentSeparator +{ + type => "config", + comment => "SecArgumentSeparator (get-pos)", + conf => q( + SecRuleEngine On + SecArgumentSeparator ";" + SecRule ARGS:a "@streq 1" "phase:1,deny,chain" + 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" + 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" + 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" + SecRule ARGS:b "@streq 2" "phase:2,deny" + ), + 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 + SecAuditEngine On + SecAuditLogParts ABCDEFGHIJKZ + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecAuditLog $ENV{AUDIT_LOG} + SecRequestBodyAccess On + SecRule ARGS:a "\@streq 1" "phase:2,deny,chain" + 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 + SecAuditEngine On + SecAuditLogParts ABCDEFGHIJKZ + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecAuditLog $ENV{AUDIT_LOG} + SecRequestBodyAccess Off + SecRule ARGS:a "\@streq 1" "phase:2,deny" + SecRule ARGS:b "\@streq 2" "phase:2,deny" + ), + 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", + ), +}, diff --git a/apache2/t/regression/rule/00-disruptive-actions.t b/apache2/t/regression/rule/00-disruptive-actions.t new file mode 100644 index 00000000..a44d3721 --- /dev/null +++ b/apache2/t/regression/rule/00-disruptive-actions.t @@ -0,0 +1,427 @@ +### Pass +{ + type => "rule", + comment => "pass action in phase:1", + conf => qq( + SecRuleEngine On + SecAction "phase:1,pass" + SecAction "phase:1,deny" + ), + 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 => "rule", + comment => "pass action in phase:2", + conf => qq( + SecRuleEngine On + SecAction "phase:2,pass" + SecAction "phase:2,deny" + ), + 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 => "rule", + comment => "pass action in phase:3", + conf => qq( + SecRuleEngine On + SecAction "phase:3,pass" + SecAction "phase:3,deny" + ), + 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 => "rule", + comment => "pass action in phase:4", + conf => qq( + SecRuleEngine On + SecAction "phase:4,pass" + SecAction "phase:4,deny" + ), + 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 => "rule", + comment => "allow action in phase:1", + conf => qq( + SecRuleEngine On + SecAction "phase:1,allow" + SecAction "phase:1,deny" + ), + 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 => "rule", + comment => "allow action in phase:2", + conf => qq( + SecRuleEngine On + SecAction "phase:2,allow" + SecAction "phase:2,deny" + ), + 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 => "rule", + comment => "allow action in phase:3", + conf => qq( + SecRuleEngine On + SecAction "phase:3,allow" + SecAction "phase:3,deny" + ), + 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 => "rule", + comment => "allow action in phase:4", + conf => qq( + SecRuleEngine On + SecAction "phase:4,allow" + SecAction "phase:4,deny" + ), + 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 => "rule", + comment => "deny action in phase:1", + conf => qq( + SecRuleEngine On + SecAction "phase:1,deny" + ), + 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 => "rule", + comment => "deny action in phase:2", + conf => qq( + SecRuleEngine On + SecAction "phase:2,deny" + ), + 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 => "rule", + comment => "deny action in phase:3", + conf => qq( + SecRuleEngine On + SecAction "phase:3,deny" + ), + 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 => "rule", + comment => "deny action in phase:4", + conf => qq( + SecRuleEngine On + SecAction "phase:4,deny" + ), + 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 => "rule", + comment => "drop action in phase:1", + conf => qq( + SecRuleEngine On + SecAction "phase:1,drop" + ), + 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 => "rule", + comment => "drop action in phase:2", + conf => qq( + SecRuleEngine On + SecAction "phase:2,drop" + ), + 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 => "rule", + comment => "drop action in phase:3", + conf => qq( + SecRuleEngine On + SecAction "phase:3,drop" + ), + 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 => "rule", + comment => "drop action in phase:4", + conf => qq( + SecRuleEngine On + SecAction "phase:4,drop" + ), + 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 => "rule", + comment => "redirect action in phase:1 (get)", + conf => qq( + SecRuleEngine On + SecRule REQUEST_URI "\@streq /test2.txt" "phase:1,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 1\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "rule", + comment => "redirect action in phase:2 (get)", + conf => qq( + SecRuleEngine On + SecRule REQUEST_URI "\@streq /test2.txt" "phase:2,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 2\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "rule", + comment => "redirect action in phase:3 (get)", + conf => qq( + SecRuleEngine On + SecRule REQUEST_URI "\@streq /test2.txt" "phase:3,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 3\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "rule", + comment => "redirect action in phase:4 (get)", + conf => qq( + SecRuleEngine On + SecRule REQUEST_URI "\@streq /test2.txt" "phase:4,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 4\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, + +### Proxy +{ + type => "rule", + comment => "proxy action in phase:1 (get)", + conf => qq( + SecRuleEngine On + SecRule REQUEST_URI "\@streq /test2.txt" "phase:1,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied using proxy to \(phase 1\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "rule", + comment => "proxy action in phase:2 (get)", + conf => qq( + SecRuleEngine On + SecRule REQUEST_URI "\@streq /test2.txt" "phase:2,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied using proxy to \(phase 2\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "rule", + comment => "proxy action in phase:3 (get)", + conf => qq( + SecRuleEngine On + SecRule REQUEST_URI "\@streq /test2.txt" "phase:3,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 500 \(phase 3\) \(Configuration Error: Proxy action requested but it does not work in output phases\)./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "rule", + comment => "proxy action in phase:4 (get)", + conf => qq( + SecRuleEngine On + SecRule REQUEST_URI "\@streq /test2.txt" "phase:4,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 500 \(phase 4\) \(Configuration Error: Proxy action requested but it does not work in output phases\)./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, diff --git a/apache2/t/run-regression-tests.pl.in b/apache2/t/run-regression-tests.pl.in index 460efb15..e515929d 100755 --- a/apache2/t/run-regression-tests.pl.in +++ b/apache2/t/run-regression-tests.pl.in @@ -1,4 +1,3 @@ -#!/usr/bin/perl #!@PERL@ # # Run regression tests. @@ -71,7 +70,7 @@ EOT usage() if ($opt{h}); ### Check startup script -$opt{a} = "apachectl" unless (defined $opt{a}); +$opt{a} = "httpd" unless (defined $opt{a}); usage("Invalid Apache startup script: $opt{a}\n") unless (-e $opt{a}); ### Defaults @@ -181,7 +180,7 @@ sub runfile { } if ($httpd_up) { - # Perform the request + # Perform the request and check response if (exists $t{request}) { my $resp = do_request($t{request}); if (!$resp) { @@ -189,21 +188,45 @@ sub runfile { dbg("RESPONSE: ", $resp); $rc = 1; } - elsif (exists $t{match_response}{status}) { - unless ($resp->code =~ m/$t{match_response}{status}/) { - msg("incorrect status code " . $resp->code . ": $t{match_response}{status}"); - $rc = 1; + else { + for my $key (keys %{ $t{match_response} || {}}) { + my($neg,$mtype) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_response}{$key}; + my $match = match_response($mtype, $resp, $m); + if ($neg and defined $match) { + $rc = 1; + msg("response $mtype matched: $m"); + dbg("$LOG{$mtype}{buf}"); + + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("response $mtype no match: $m"); + dbg("$LOG{$mtype}{buf}"); + last; + } } } } # Search for all log matches if ($rc == 0 and exists $t{match_log} and defined $t{match_log}) { - for my $mtype (keys %{ $t{match_log} || {}}) { - my $m = $t{match_log}{$mtype}; - unless (defined log_read_match($mtype, @{$m || []})) { + for my $key (keys %{ $t{match_log} || {}}) { + my($neg,$mtype) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_log}{$key}; + my $match = match_log($mtype, @{$m || []}); + if ($neg and defined $match) { $rc = 1; - msg("$mtype log match failed: $m->[0]"); + msg("$mtype log matched: $m->[0]"); + dbg("$LOG{$mtype}{buf}"); + + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("$mtype log no match: $m->[0]"); + dbg("$LOG{$mtype}{buf}"); last; } } @@ -251,12 +274,30 @@ sub do_request { return; } -sub log_read_match { + +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/m); + } + elsif ($name eq "content") { + return $@ if ($resp->content =~ m/$re/m); + } + + return; +} + +sub match_log { my($name, $re, $timeout) = @_; my $t0 = gettimeofday(); my($fh,$rbuf) = ($LOG{$name}{fd}, \$LOG{$name}{buf}); my $n = length($$rbuf); + msg("Warning: Empty regular expression.") if (!defined $re or $re eq ""); + $timeout = 0 unless (defined $timeout); do { @@ -267,7 +308,7 @@ sub log_read_match { sleep 0.1; } while (gettimeofday - $t0 < $timeout); - return undef; + return; } sub escape { @@ -288,7 +329,11 @@ sub dbg { } sub msg { - print STDOUT "@_\n" if (@_); + return unless(@_); + my $out = join "", map { + (ref $_ ne "" ? Dumper($_) : $_) + } @_; + print STDOUT "$out\n"; } sub quit { @@ -321,7 +366,7 @@ sub httpd_start { -k => "start", ); - #dbg("EXEC: ", \@p); +# dbg("EXEC: ", \@p); # dbg("Httpd start"); my $httpd_out; @@ -350,7 +395,7 @@ sub httpd_start { } # Look for startup msg - unless (defined log_read_match("error", qr/resuming normal operations/, 10)) { + unless (defined match_log("error", qr/resuming normal operations/, 10)) { quit(1, "Httpd server failed to start."); } @@ -396,7 +441,7 @@ sub httpd_stop { } # Look for startup msg - unless (defined log_read_match("error", qr/caught SIG[A-Z]+, shutting down/, 10)) { + unless (defined match_log("error", qr/caught SIG[A-Z]+, shutting down/, 10)) { quit(1, "Httpd server failed to shutdown."); } @@ -442,7 +487,7 @@ sub httpd_reload { } # Look for startup msg - unless (defined log_read_match("error", qr/resuming normal operations/, 10)) { + unless (defined match_log("error", qr/resuming normal operations/, 10)) { quit(1, "Httpd server failed to reload."); }