From 7ad2766e7677b6392ef02aff008c467d935470bc Mon Sep 17 00:00:00 2001 From: brectanus Date: Tue, 27 May 2008 23:52:16 +0000 Subject: [PATCH] Some more updates for regression testing. --- .../regression/action/00-disruptive-actions.t | 14 +- .../t/regression/config/10-audit-directives.t | 82 ++++++++++ .../t/regression/config/10-misc-directives.t | 48 ++++++ .../regression/config/10-request-directives.t | 11 +- .../config/10-response-directives.t | 145 ++++++++++++++++++ .../t/regression/server_root/htdocs/8k.txt | Bin 0 -> 8192 bytes apache2/t/run-regression-tests.pl.in | 51 ++++-- 7 files changed, 325 insertions(+), 26 deletions(-) create mode 100644 apache2/t/regression/config/10-response-directives.t create mode 100644 apache2/t/regression/server_root/htdocs/8k.txt diff --git a/apache2/t/regression/action/00-disruptive-actions.t b/apache2/t/regression/action/00-disruptive-actions.t index bc2521f0..0d13bd7a 100644 --- a/apache2/t/regression/action/00-disruptive-actions.t +++ b/apache2/t/regression/action/00-disruptive-actions.t @@ -1,4 +1,6 @@ -### Pass +### Tests all of the actions in each phase + +# Pass { type => "action", comment => "pass action in phase:1", @@ -72,7 +74,7 @@ ), }, -### Allow +# Allow { type => "action", comment => "allow action in phase:1", @@ -146,7 +148,7 @@ ), }, -### Deny +# Deny { type => "action", comment => "deny action in phase:1", @@ -216,7 +218,7 @@ ), }, -### Drop +# Drop { type => "action", comment => "drop action in phase:1", @@ -286,7 +288,7 @@ ), }, -### Redirect +# Redirect { type => "action", comment => "redirect action in phase:1 (get)", @@ -360,7 +362,7 @@ ), }, -### Proxy +# Proxy { type => "action", comment => "proxy action in phase:1 (get)", diff --git a/apache2/t/regression/config/10-audit-directives.t b/apache2/t/regression/config/10-audit-directives.t index e79c21f8..6769a6a9 100644 --- a/apache2/t/regression/config/10-audit-directives.t +++ b/apache2/t/regression/config/10-audit-directives.t @@ -1,4 +1,6 @@ ### SecAudit* directive tests + +# SecAuditEngine { type => "config", comment => "SecAuditEngine On", @@ -75,6 +77,84 @@ 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 :) + ### + + # 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; + 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 + $LOG{$id}{fd} = new FileHandle($alogdatafn, O_RDONLY); + $LOG{$id}{fd}->blocking(0); + $LOG{$id}{buf} = ""; + my $alogdata = match_log($id, qr/^--[^-]+-A--.*$id.*-Z--$/s, 1); + if (defined $alogdata) { + $LOG{$id}{fd}->close(); + delete $LOG{$id}; + return 0; + } + + # Error + dbg("LOGDATA: \"$alogdata\""); + die "Audit log data did not match.\n"; + }, + 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)", @@ -111,6 +191,8 @@ GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", ), }, + +# SecAuditLogParts { type => "config", comment => "SecAuditLogParts (minimal)", diff --git a/apache2/t/regression/config/10-misc-directives.t b/apache2/t/regression/config/10-misc-directives.t index f2175d14..1ceae18a 100644 --- a/apache2/t/regression/config/10-misc-directives.t +++ b/apache2/t/regression/config/10-misc-directives.t @@ -1 +1,49 @@ ### Misc directive tests + +### TODO: +# SecTmpDir +# SecUploadDir +# SecUploadKeepFiles +# SecWebAppId +# SecDataDir +# SecChrootDir +# SecGuardianLog + +# SecServerSignature +{ + type => "config", + comment => "SecServerSignature On", + conf => qq( + SecServerSignature "NewServerSignature" + ), + match_log => { + error => [ qr/NewServerSignature/, 1 ], + }, + match_response => { + status => qr/^200$/, + raw => qr/^Server: +NewServerSignature$/m, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecDefaultAction +{ + type => "config", + comment => "SecServerSignature On", + conf => qq( + SecRuleEngine on + SecDefaultAction "phase:1,deny,status:500" + SecRule REQUEST_URI "test.txt" + ), + 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", + ), +}, diff --git a/apache2/t/regression/config/10-request-directives.t b/apache2/t/regression/config/10-request-directives.t index b951676d..97bd0cb8 100644 --- a/apache2/t/regression/config/10-request-directives.t +++ b/apache2/t/regression/config/10-request-directives.t @@ -1,5 +1,6 @@ +### Tests for directives altering how a request is handled -### SecArgumentSeparator +# SecArgumentSeparator { type => "config", comment => "SecArgumentSeparator (get-pos)", @@ -85,7 +86,7 @@ ), }, -### SecRequestBodyAccess +# SecRequestBodyAccess { type => "config", comment => "SecRequestBodyAccess (pos)", @@ -133,7 +134,7 @@ ), }, -### SecRequestBodyLimit +# SecRequestBodyLimit { type => "config", comment => "SecRequestBodyLimit (equal)", @@ -179,7 +180,7 @@ ), }, -### SecRequestBodyInMemoryLimit +# SecRequestBodyInMemoryLimit { type => "config", comment => "SecRequestBodyInMemoryLimit (equal)", @@ -249,7 +250,7 @@ Content-Disposition: form-data; name="b" -----------------------------69343412719991675451336310646--), 1024), }, -### SecCookieFormat +# SecCookieFormat { type => "config", comment => "SecCookieFormat (pos)", diff --git a/apache2/t/regression/config/10-response-directives.t b/apache2/t/regression/config/10-response-directives.t new file mode 100644 index 00000000..894b06ec --- /dev/null +++ b/apache2/t/regression/config/10-response-directives.t @@ -0,0 +1,145 @@ +### Tests for directives altering how a response is handled + +# SecResponseBodyAccess +{ + type => "config", + comment => "SecResponseBodyAccess (pos)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule RESPONSE_BODY "TEST" "phase:4,deny" + ), + 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 (neg)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess Off + SecResponseBodyMimeType null + SecRule RESPONSE_BODY "TEST" "phase:4,deny" + ), + 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", + ), +}, + +# SecResponseBodyLimit +{ + type => "config", + comment => "SecResponseBodyLimit (equal)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType 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 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 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 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 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/apache2/t/regression/server_root/htdocs/8k.txt b/apache2/t/regression/server_root/htdocs/8k.txt new file mode 100644 index 0000000000000000000000000000000000000000..6d17cf9d15fb9f4a2358a2d079f3b8c755d005fa GIT binary patch literal 8192 zcmeIu0Sy2E0K%a6Pi+o2h(KY$fB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM GyblZ@00031 literal 0 HcmV?d00001 diff --git a/apache2/t/run-regression-tests.pl.in b/apache2/t/run-regression-tests.pl.in index 166dd3e6..8dc90db5 100755 --- a/apache2/t/run-regression-tests.pl.in +++ b/apache2/t/run-regression-tests.pl.in @@ -1,3 +1,4 @@ +#!/usr/bin/perl #!@PERL@ # # Run regression tests. @@ -29,6 +30,7 @@ my $SROOT_DIR = "$REG_DIR/server_root"; my $CONF_DIR = "$SROOT_DIR/conf"; my $LOGS_DIR = "$SROOT_DIR/logs"; my $PID_FILE = "$LOGS_DIR/httpd.pid"; +my $HTTPD = q(@APXS_HTTPD@); my $PASSED = 0; my $TOTAL = 0; my %C = (); @@ -37,6 +39,11 @@ 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"; +} + $SIG{TERM} = $SIG{INT} = \&handle_interrupt; my %opt; @@ -74,8 +81,13 @@ EOT usage() if ($opt{h}); ### Check httpd binary -$opt{a} = "@APXS_HTTPD@" unless (defined $opt{a}); -usage("Invalid Apache startup script: $opt{a}\n") unless (-e $opt{a}); +if (defined $opt{a}) { + $HTTPD = $opt{a}; +} +else { + $opt{a} = $HTTPD; +} +usage("Invalid Apache startup script: $HTTPD\n") unless (-e $HTTPD); ### Defaults $opt{A} = "$LOGS_DIR/modsec_audit.log" unless (defined $opt{A}); @@ -86,7 +98,7 @@ $opt{H} = "$SROOT_DIR/htdocs" unless (defined $opt{H}); $opt{p} = 8088 unless (defined $opt{p}); unless (defined $opt{S}) { - my $httpd_root = `$opt{a} -V`; + my $httpd_root = `$HTTPD -V`; ($opt{S} = $httpd_root) =~ s/.*-D HTTPD_ROOT="([^"]*)".*/$1/sm; } @@ -178,7 +190,8 @@ sub runfile { $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" ? &{$t{conf}} : $t{conf}); + print CONF (ref $t{conf} eq "CODE" ? eval { &{$t{conf}} } : $t{conf}); + msg("$@") if ($@); close CONF; $httpd_up = httpd_start("Include $conf_fn") ? 0 : 1; } @@ -223,15 +236,15 @@ sub runfile { } } } - else { - msg("Failed to start httpd."); - $rc = 1; - } # Run any arbitrary perl tests if ($rc == 0 and exists $t{test} and defined $t{test}) { dbg("Executing perl test(s)..."); - $rc = &{$t{test}}; + $rc = eval { &{$t{test}} }; + if (! defined $rc) { + msg("Error running test: $@"); + $rc = -1; + } dbg("Perl tests returned: $rc"); } @@ -257,6 +270,10 @@ sub runfile { } } } + else { + msg("Failed to start httpd."); + $rc = 1; + } if ($rc == 0) { $pass++; @@ -307,7 +324,8 @@ sub do_request { # Allow test to execute code if (ref $r eq "CODE") { - $r = &$r; + $r = eval { &$r }; + msg("$@") unless (defined $r); } if (ref $r eq "HTTP::Request") { @@ -334,13 +352,16 @@ sub match_response { 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 match_log { my($name, $re, $timeout) = @_; - my $t0 = gettimeofday(); + my $t0 = gettimeofday; my($fh,$rbuf) = ($LOG{$name}{fd}, \$LOG{$name}{buf}); my $n = length($$rbuf); @@ -350,7 +371,7 @@ sub match_log { do { $n += $fh->sysread($$rbuf, 1024, $n); -# dbg("Match \"$re\" in \"$$rbuf\" ($n)"); +# dbg("Match \"$re\" in $name \"$$rbuf\" ($n)"); return $& if ($$rbuf =~ m/$re/m); # TODO: Use select()/poll() sleep 0.1; @@ -413,7 +434,7 @@ sub done { sub httpd_start { httpd_reset_logs(); my @p = ( - $opt{a}, + $HTTPD, -d => $opt{S}, -f => $opt{C}, (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), @@ -459,7 +480,7 @@ sub httpd_start { sub httpd_stop { httpd_reset_logs(); my @p = ( - $opt{a}, + $HTTPD, -d => $opt{S}, -f => $opt{C}, (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), @@ -505,7 +526,7 @@ sub httpd_stop { sub httpd_reload { httpd_reset_logs(); my @p = ( - $opt{a}, + $HTTPD, -d => $opt{S}, -f => $opt{C}, (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)),