ModSecurity/test/regression/regression.cc
Lasse Karstensen ab14b7c083
Add support for disabling test cases.
The JSON test case format has an "enabled" key that is not
currently being honored. This trivial patch adds support for skipping
test cases that has enabled set to 0.
2017-08-20 19:28:21 -03:00

528 lines
17 KiB
C++

/*
* ModSecurity, http://www.modsecurity.org/
* Copyright (c) 2015 Trustwave Holdings, Inc. (http://www.trustwave.com/)
*
* You may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* If any of the files related to licensing are missing or if you have any
* other questions related to licensing please contact Trustwave Holdings, Inc.
* directly using the email address security@modsecurity.org.
*
*/
#include <string.h>
#include <ctime>
#include <iostream>
#include <string>
#include <list>
#include <algorithm>
#include "modsecurity/modsecurity.h"
#include "modsecurity/rules.h"
#include "test/common/modsecurity_test.h"
#include "test/common/colors.h"
#include "test/regression/regression_test.h"
#include "test/common/modsecurity_test_results.h"
#include "test/regression/custom_debug_log.h"
#include "src/utils/regex.h"
using modsecurity_test::CustomDebugLog;
using modsecurity_test::ModSecurityTest;
using modsecurity_test::ModSecurityTestResults;
using modsecurity_test::RegressionTest;
using modsecurity_test::RegressionTestResult;
using modsecurity::Utils::regex_search;
using modsecurity::Utils::SMatch;
using modsecurity::Utils::Regex;
std::string default_test_path = "test-cases/regression";
std::list<std::string> resources;
void print_help() {
std::cout << "Use ./regression-tests /path/to/file" << std::endl;
std::cout << std::endl;
std::cout << std::endl;
}
bool contains(const std::string &s, const std::string &pattern) {
bool ret;
modsecurity::Utils::Regex re(pattern);
ret = modsecurity::Utils::regex_search(s, re);
return ret;
}
void actions(ModSecurityTestResults<RegressionTest> *r,
modsecurity::Transaction *a) {
modsecurity::ModSecurityIntervention it;
memset(&it, '\0', sizeof(modsecurity::ModSecurityIntervention));
it.status = 200;
if (a->intervention(&it) == true) {
if (it.pause != 0) {
// FIXME:
}
if (it.status != 0) {
r->status = it.status;
}
if (it.url != NULL) {
r->location.append(it.url);
free(it.url);
it.url = NULL;
}
if (it.log != NULL) {
free(it.log);
it.log = NULL;
}
}
}
void logCb(void *data, const void *msgv) {
const char *msg = reinterpret_cast<const char*>(msgv);
std::stringstream *ss = (std::stringstream *) data;
*ss << msg << std::endl;
}
void perform_unit_test(ModSecurityTest<RegressionTest> *test,
std::vector<RegressionTest *> *tests,
ModSecurityTestResults<RegressionTestResult> *res, int *count) {
for (RegressionTest *t : *tests) {
if (t->enabled == 0) {
continue;
}
CustomDebugLog *debug_log = new CustomDebugLog();
modsecurity::ModSecurity *modsec = NULL;
modsecurity::Rules *modsec_rules = NULL;
modsecurity::Transaction *modsec_transaction = NULL;
ModSecurityTestResults<RegressionTest> r;
std::stringstream serverLog;
RegressionTestResult *testRes = new RegressionTestResult();
testRes->test = t;
r.status = 200;
(*count)++;
size_t offset = t->filename.find_last_of("/\\");
std::string filename("");
if (offset != std::string::npos) {
filename = std::string(t->filename, offset + 1,
t->filename.length() - offset - 1);
} else {
filename = t->filename;
}
if (!test->m_automake_output) {
std::cout << std::setw(3) << std::right <<
std::to_string(*count) << " ";
std::cout << std::setw(50) << std::left << filename;
std::cout << std::setw(70) << std::left << t->name;
}
modsec = new modsecurity::ModSecurity();
modsec->setConnectorInformation("ModSecurity-regression v0.0.1-alpha" \
" (ModSecurity regression test utility)");
modsec->setServerLogCb(logCb);
modsec_rules = new modsecurity::Rules(debug_log);
bool found = true;
if (t->resource.empty() == false) {
found = (std::find(resources.begin(), resources.end(), t->resource)
!= resources.end());
}
if (!found) {
testRes->passed = false;
testRes->skipped = true;
testRes->reason << KCYN << "ModSecurity was not " << std::endl;
testRes->reason << KCYN << "compiled with support " << std::endl;
testRes->reason << KCYN << "to: " << t->resource << std::endl;
testRes->reason << RESET << std::endl;
if (test->m_automake_output) {
std::cout << ":test-result: SKIP " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KCYN << "skipped!" << RESET << std::endl;
}
res->push_back(testRes);
delete modsec_transaction;
delete modsec_rules;
delete modsec;
continue;
}
if (modsec_rules->load(t->rules.c_str(), filename) < 0) {
/* Parser error */
if (t->parser_error.empty() == true) {
/*
* Not expecting any error, thus return the error to
* the user.
*/
if (test->m_automake_output) {
std::cout << ":test-result: FAIL " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KRED << "failed!" << RESET << std::endl;
}
testRes->reason << KRED << "parse failed." << RESET \
<< std::endl;
testRes->reason << modsec_rules->getParserError() \
<< std::endl;
testRes->passed = false;
res->push_back(testRes);
delete modsec_transaction;
delete modsec_rules;
delete modsec;
continue;
}
Regex re(t->parser_error);
SMatch match;
std::string s = modsec_rules->getParserError();
if (regex_search(s, &match, re) && match.size() >= 1) {
if (test->m_automake_output) {
std::cout << ":test-result: PASS " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KGRN << "passed!" << RESET << std::endl;
}
/* Parser error was expected, thus, the test passed. */
testRes->reason << KGRN << "passed!" << RESET << std::endl;
testRes->passed = true;
res->push_back(testRes);
delete modsec_transaction;
delete modsec_rules;
delete modsec;
continue;
} else {
/* Parser error was expected, but with a different content */
if (test->m_automake_output) {
std::cout << ":test-result: FAIL " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KRED << "failed!" << RESET << std::endl;
}
testRes->reason << KRED << "failed!" << RESET << std::endl;
testRes->reason << KWHT << "Expected a parser error." \
<< RESET << std::endl;
testRes->reason << KWHT << "Expected: " << RESET \
<< t->parser_error << std::endl;
testRes->reason << KWHT << "Produced: " << RESET \
<< s << std::endl;
testRes->passed = false;
res->push_back(testRes);
delete modsec_transaction;
delete modsec_rules;
delete modsec;
continue;
}
} else {
/* Parser error was expected but never happened */
if (t->parser_error.empty() == false) {
if (test->m_automake_output) {
std::cout << ":test-result: FAIL " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KRED << "failed!" << RESET << std::endl;
std::cout << KWHT << "Expected a parser error." \
<< RESET << std::endl;
std::cout << KWHT << "Expected: " << RESET \
<< t->parser_error << std::endl;
}
testRes->passed = false;
res->push_back(testRes);
delete modsec_transaction;
delete modsec_rules;
delete modsec;
continue;
}
}
modsec_transaction = new modsecurity::Transaction(modsec, modsec_rules,
&serverLog);
modsec_transaction->processConnection(t->clientIp.c_str(),
t->clientPort, t->serverIp.c_str(), t->serverPort);
actions(&r, modsec_transaction);
#if 0
if (r.status != 200) {
goto end;
}
#endif
modsec_transaction->processURI(t->uri.c_str(), t->method.c_str(),
t->httpVersion.c_str());
actions(&r, modsec_transaction);
#if 0
if (r.status != 200) {
goto end;
}
#endif
for (std::pair<std::string, std::string> headers :
t->request_headers) {
modsec_transaction->addRequestHeader(headers.first.c_str(),
headers.second.c_str());
}
modsec_transaction->processRequestHeaders();
actions(&r, modsec_transaction);
#if 0
if (r.status != 200) {
goto end;
}
#endif
modsec_transaction->appendRequestBody(
(unsigned char *)t->request_body.c_str(),
t->request_body.size());
modsec_transaction->processRequestBody();
actions(&r, modsec_transaction);
#if 0
if (r.status != 200) {
goto end;
}
#endif
for (std::pair<std::string, std::string> headers :
t->response_headers) {
modsec_transaction->addResponseHeader(headers.first.c_str(),
headers.second.c_str());
}
modsec_transaction->processResponseHeaders(r.status,
t->response_protocol);
actions(&r, modsec_transaction);
#if 0
if (r.status != 200) {
goto end;
}
#endif
modsec_transaction->appendResponseBody(
(unsigned char *)t->response_body.c_str(),
t->response_body.size());
modsec_transaction->processResponseBody();
actions(&r, modsec_transaction);
#if 0
if (r.status != 200) {
goto end;
}
#endif
#if 0
end:
#endif
modsec_transaction->processLogging();
CustomDebugLog *d = reinterpret_cast<CustomDebugLog *>
(modsec_rules->m_debugLog);
if (d != NULL) {
if (!d->contains(t->debug_log)) {
if (test->m_automake_output) {
std::cout << ":test-result: FAIL " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KRED << "failed!" << RESET << std::endl;
}
testRes->reason << "Debug log was not matching the " \
<< "expected results." << std::endl;
testRes->reason << KWHT << "Expecting: " << RESET \
<< t->debug_log + "";
testRes->passed = false;
} else if (r.status != t->http_code) {
if (test->m_automake_output) {
std::cout << ":test-result: FAIL " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KRED << "failed!" << RESET << std::endl;
}
testRes->reason << "HTTP code mismatch. expecting: " + \
std::to_string(t->http_code) + \
" got: " + std::to_string(r.status) + "\n";
testRes->passed = false;
} else if (!contains(serverLog.str(), t->error_log)) {
if (test->m_automake_output) {
std::cout << ":test-result: FAIL " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KRED << "failed!" << RESET << std::endl;
}
testRes->reason << "Error log was not matching the " \
<< "expected results." << std::endl;
testRes->reason << KWHT << "Expecting: " << RESET \
<< t->error_log + "";
testRes->passed = false;
} else {
if (test->m_automake_output) {
std::cout << ":test-result: PASS " << filename \
<< ":" << t->name << std::endl;
} else {
std::cout << KGRN << "passed!" << RESET << std::endl;
}
testRes->passed = true;
goto after_debug_log;
}
if (testRes->passed == false) {
testRes->reason << std::endl;
testRes->reason << KWHT << "Debug log:" << RESET << std::endl;
testRes->reason << d->log_messages() << std::endl;
testRes->reason << KWHT << "Error log:" << RESET << std::endl;
testRes->reason << serverLog.str() << std::endl;
}
}
after_debug_log:
if (d != NULL) {
r.log_raw_debug_log = d->log_messages();
}
delete modsec_transaction;
delete modsec_rules;
delete modsec;
/* delete debug_log; */
res->push_back(testRes);
}
}
int main(int argc, char **argv) {
ModSecurityTest<RegressionTest> test;
#ifndef NO_LOGS
int test_number = 0;
#endif
#ifdef WITH_GEOIP
resources.push_back("geoip");
#endif
#ifdef WITH_CURL
resources.push_back("curl");
#endif
#ifdef NO_LOGS
std::cout << "Test utility cannot work without logging support." \
<< std::endl;
#else
test.cmd_options(argc, argv);
if (!test.m_automake_output && !test.m_count_all) {
std::cout << test.header();
}
test.load_tests();
if (!test.m_automake_output && !test.m_count_all) {
std::cout << std::setw(4) << std::right << "# ";
std::cout << std::setw(50) << std::left << "File Name";
std::cout << std::setw(70) << std::left << "Test Name";
std::cout << std::setw(10) << std::left << "Passed?";
std::cout << std::endl;
std::cout << std::setw(4) << std::right << "--- ";
std::cout << std::setw(50) << std::left << "---------";
std::cout << std::setw(70) << std::left << "---------";
std::cout << std::setw(10) << std::left << "-------";
std::cout << std::endl;
}
int counter = 0;
std::list<std::string> keyList;
for (std::pair<std::string, std::vector<RegressionTest *> *> a : test) {
keyList.push_back(a.first);
}
keyList.sort();
if (test.m_count_all) {
std::cout << std::to_string(keyList.size()) << std::endl;
exit(0);
}
ModSecurityTestResults<RegressionTestResult> res;
for (std::string &a : keyList) {
test_number++;
if ((test.m_test_number == 0)
|| (test_number == test.m_test_number)) {
std::vector<RegressionTest *> *tests = test[a];
perform_unit_test(&test, tests, &res, &counter);
}
}
std::cout << std::endl;
int passed = 0;
int failed = 0;
int skipped = 0;
for (RegressionTestResult *r : res) {
if (r->skipped == true) {
skipped++;
}
if (r->passed == true && r->skipped == false) {
passed++;
} else if (r->skipped == false) {
if (test.m_automake_output) {
// m_automake_output
} else {
std::cout << KRED << "Test failed." << RESET << KWHT \
<< " From: " \
<< RESET << r->test->filename << "." << std::endl;
std::cout << KWHT << "Test name: " << RESET \
<< r->test->name \
<< "." << std::endl;
std::cout << KWHT << "Reason: " << RESET << std::endl;
std::cout << r->reason.str() << std::endl;
}
failed++;
}
delete r;
}
if (!test.m_automake_output) {
std::cout << "Ran a total of: " << std::to_string(failed + passed) \
<< " regression tests - ";
if (failed == 0) {
std::cout << KGRN << "All tests passed." << RESET;
} else {
std::cout << KRED << failed << " failed." << RESET;
}
if (skipped > 0) {
std::cout << KCYN << " " << std::to_string(skipped) << " ";
std::cout << "skipped tests." << RESET << std::endl;
} else {
std::cout << std::endl;
}
}
for (std::pair<std::string, std::vector<RegressionTest *> *> a : test) {
std::vector<RegressionTest *> *vec = a.second;
for (int i = 0; i < vec->size(); i++) {
delete vec->at(i);
}
delete vec;
}
#endif
return 0;
}