mirror of https://github.com/sysown/proxysql
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
7.6 KiB
203 lines
7.6 KiB
/**
|
|
* @file genai_plugin_anomaly_unit-t.cpp
|
|
* @brief Unit tests for the carved-out plugin-side Anomaly_Detector.
|
|
*
|
|
* Step 3 of the GenAI plugin carve-out moved Anomaly_Detector from
|
|
* lib/ into plugins/genai/. This file replaces the old in-core unit
|
|
* test (test/tap/tests/unit/genai_anomaly_unit-t.cpp) and exercises
|
|
* the plugin-side copy.
|
|
*
|
|
* Build trick: the test compiles plugins/genai/src/Anomaly_Detector.cpp
|
|
* directly into the test binary (see the rule in this directory's
|
|
* Makefile) so it can call private methods through a friend helper
|
|
* without having to dlopen the .so or expose extra symbols. The
|
|
* plugin's own .so still gets built and shipped from
|
|
* plugins/genai/Makefile in the normal way; this is a test-only
|
|
* second compile.
|
|
*/
|
|
|
|
#include "tap.h"
|
|
#include "test_globals.h"
|
|
#include "test_init.h"
|
|
|
|
#include "Anomaly_Detector.h"
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
/**
|
|
* @brief Friend-class helper that exposes private detector methods
|
|
* for testing. Declared as a friend in Anomaly_Detector.h.
|
|
*/
|
|
class Anomaly_Detector_TestHelper {
|
|
public:
|
|
static std::string normalize_query(Anomaly_Detector& d, const std::string& query) {
|
|
return d.normalize_query(query);
|
|
}
|
|
static AnomalyResult check_sql_injection(Anomaly_Detector& d, const std::string& query) {
|
|
return d.check_sql_injection(query);
|
|
}
|
|
};
|
|
|
|
// ============================================================
|
|
// normalize_query() — SQL normalization
|
|
// ============================================================
|
|
|
|
static void test_normalize_basic() {
|
|
Anomaly_Detector d;
|
|
std::string r = Anomaly_Detector_TestHelper::normalize_query(d, "SELECT * FROM users WHERE id = 42");
|
|
ok(r.find("42") == std::string::npos, "normalize: numeric literal replaced");
|
|
ok(r.find("select") != std::string::npos, "normalize: query is lowercased");
|
|
ok(r.find("from") != std::string::npos, "normalize: query structure preserved");
|
|
}
|
|
|
|
static void test_normalize_string_literals() {
|
|
Anomaly_Detector d;
|
|
std::string r = Anomaly_Detector_TestHelper::normalize_query(d, "SELECT * FROM t WHERE name = 'alice'");
|
|
ok(r.find("alice") == std::string::npos, "normalize: string literal replaced");
|
|
ok(r.find("?") != std::string::npos, "normalize: string literal replaced with ?");
|
|
}
|
|
|
|
static void test_normalize_double_quoted_strings() {
|
|
Anomaly_Detector d;
|
|
std::string r = Anomaly_Detector_TestHelper::normalize_query(d, "SELECT * FROM users WHERE name = \"bob\"");
|
|
ok(r.find("bob") == std::string::npos, "normalize: double-quoted literal replaced");
|
|
}
|
|
|
|
static void test_normalize_whitespace() {
|
|
Anomaly_Detector d;
|
|
std::string r = Anomaly_Detector_TestHelper::normalize_query(d, "SELECT * FROM users WHERE id = 1");
|
|
ok(r.find(" ") == std::string::npos, "normalize: extra whitespace collapsed");
|
|
}
|
|
|
|
static void test_normalize_comments() {
|
|
Anomaly_Detector d;
|
|
std::string r = Anomaly_Detector_TestHelper::normalize_query(d, "SELECT * FROM users /* comment */ WHERE id = 1");
|
|
ok(r.find("comment") == std::string::npos, "normalize: block comment removed");
|
|
}
|
|
|
|
static void test_normalize_empty() {
|
|
Anomaly_Detector d;
|
|
std::string r = Anomaly_Detector_TestHelper::normalize_query(d, "");
|
|
ok(r.empty(), "normalize: empty query gives empty result");
|
|
}
|
|
|
|
static void test_normalize_multiple_literals() {
|
|
Anomaly_Detector d;
|
|
std::string r = Anomaly_Detector_TestHelper::normalize_query(d, "INSERT INTO users (name, age) VALUES ('alice', 30)");
|
|
ok(r.find("alice") == std::string::npos, "normalize: first string literal replaced");
|
|
ok(r.find("30") == std::string::npos, "normalize: second numeric literal replaced");
|
|
}
|
|
|
|
// ============================================================
|
|
// check_sql_injection() — SQLi pattern detection
|
|
// ============================================================
|
|
|
|
static void test_sqli_union_select() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "SELECT * FROM users UNION SELECT * FROM passwords");
|
|
ok(r.is_anomaly == true, "check_sqli: UNION SELECT detected as anomaly");
|
|
ok(r.risk_score > 0.0f, "check_sqli: UNION SELECT has positive risk score");
|
|
ok(!r.matched_rules.empty(), "check_sqli: UNION SELECT has matched rules");
|
|
}
|
|
|
|
static void test_sqli_comment_injection() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "SELECT * FROM users; -- WHERE admin=1");
|
|
ok(r.is_anomaly == true, "check_sqli: comment injection detected");
|
|
}
|
|
|
|
static void test_sqli_safe_query() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "SELECT id, name FROM users WHERE id = ?");
|
|
ok(r.is_anomaly == false, "check_sqli: parameterized query is safe");
|
|
ok(r.risk_score == 0.0f, "check_sqli: safe query has zero risk");
|
|
}
|
|
|
|
static void test_sqli_drop_table() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "DROP TABLE users");
|
|
ok(r.is_anomaly == true, "check_sqli: DROP TABLE detected");
|
|
}
|
|
|
|
static void test_sqli_sleep() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "SELECT SLEEP(5)");
|
|
ok(r.is_anomaly == true, "check_sqli: SLEEP() suspicious keyword detected");
|
|
}
|
|
|
|
static void test_sqli_concat_attack() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "SELECT CONCAT(username, ':', password) FROM users");
|
|
ok(r.is_anomaly == true, "check_sqli: CONCAT() attack pattern detected");
|
|
}
|
|
|
|
static void test_sqli_hex_encoded() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "SELECT * FROM users WHERE name = 0x61646d696e");
|
|
ok(r.is_anomaly == true, "check_sqli: hex-encoded value detected");
|
|
}
|
|
|
|
static void test_sqli_xss_pattern() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "INSERT INTO comments (body) VALUES ('<script>alert(1)</script>')");
|
|
ok(r.is_anomaly == true, "check_sqli: XSS script tag detected");
|
|
}
|
|
|
|
static void test_sqli_risk_score_scaling() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult single = Anomaly_Detector_TestHelper::check_sql_injection(d, "UNION SELECT * FROM users");
|
|
AnomalyResult multi = Anomaly_Detector_TestHelper::check_sql_injection(d, "' OR 1=1 UNION SELECT * FROM users; -- DROP TABLE users");
|
|
ok(multi.risk_score >= single.risk_score,
|
|
"check_sqli: multiple patterns produce higher or equal risk score");
|
|
}
|
|
|
|
static void test_sqli_anomaly_type() {
|
|
Anomaly_Detector d;
|
|
AnomalyResult r = Anomaly_Detector_TestHelper::check_sql_injection(d, "UNION SELECT 1,2,3");
|
|
ok(r.anomaly_type == "sql_injection", "check_sqli: anomaly_type is 'sql_injection'");
|
|
}
|
|
|
|
// ============================================================
|
|
// analyze() pipeline
|
|
// ============================================================
|
|
|
|
static void test_analyze_pipeline_runs_without_vector_db() {
|
|
Anomaly_Detector d;
|
|
(void)d.init();
|
|
AnomalyResult r = d.analyze("SELECT 1", "alice", "127.0.0.1", "test");
|
|
ok(r.risk_score >= 0.0 && r.risk_score <= 1.0,
|
|
"analyze() returns a risk_score in [0, 1] (got %.3f)", r.risk_score);
|
|
d.close();
|
|
}
|
|
|
|
int main() {
|
|
plan(25);
|
|
|
|
test_init_minimal();
|
|
|
|
test_normalize_basic();
|
|
test_normalize_string_literals();
|
|
test_normalize_double_quoted_strings();
|
|
test_normalize_whitespace();
|
|
test_normalize_comments();
|
|
test_normalize_empty();
|
|
test_normalize_multiple_literals();
|
|
|
|
test_sqli_union_select();
|
|
test_sqli_comment_injection();
|
|
test_sqli_safe_query();
|
|
test_sqli_drop_table();
|
|
test_sqli_sleep();
|
|
test_sqli_concat_attack();
|
|
test_sqli_hex_encoded();
|
|
test_sqli_xss_pattern();
|
|
test_sqli_risk_score_scaling();
|
|
test_sqli_anomaly_type();
|
|
|
|
test_analyze_pipeline_runs_without_vector_db();
|
|
|
|
test_cleanup_minimal();
|
|
return exit_status();
|
|
}
|