Files
mysql-audit/src/audit_plugin.cc

1838 lines
62 KiB
C++

/*
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
#include "mysql_inc.h"
#include "hot_patch.h"
#include <stdlib.h>
#include <ctype.h>
#include "audit_handler.h"
#include <string.h>
#include <sys/mman.h>
#if MYSQL_VERSION_ID >= 50600
//in 5.6 md5 implementation changed and we include our own
#include "md5.h"
#endif
/*
Disable __attribute__() on non-gcc compilers.
#if !defined(__attribute__) && !defined(__GNUC__)
#define __attribute__(A)
#endif
*/
static const char * log_prefix = AUDIT_LOG_PREFIX;
//possible audit handlers
static Audit_file_handler json_file_handler;
static Audit_socket_handler json_socket_handler;
//formatters
static Audit_json_formatter json_formatter;
//flags to hold if audit handlers are enabled
static my_bool json_file_handler_enable = FALSE;
static my_bool force_record_logins_enable = FALSE;
static my_bool json_file_handler_flush = FALSE;
static my_bool json_socket_handler_enable = FALSE;
static my_bool uninstall_plugin_enable = FALSE;
static my_bool validate_checksum_enable = FALSE;
static my_bool offsets_by_version_enable = FALSE;
static my_bool validate_offsets_extended_enable = FALSE;
static char * offsets_string = NULL;
static char * checksum_string = NULL;
static int delay_ms_val =0;
static char *delay_cmds_string = NULL;
static char delay_cmds_buff[4096] = {0};
static char *whitelist_cmds_string = NULL;
static char whitelist_cmds_buff[4096] = {0};
static char *record_cmds_string = NULL;
static char record_cmds_buff[4096] = {0};
static char *password_masking_cmds_string = NULL;
static char password_masking_cmds_buff[4096] = {0};
static char *record_objs_string = NULL;
static char record_objs_buff[4096] = {0};
static char *whitelist_users_string = NULL;
static char whitelist_users_buff[4096] = {0};
static char delay_cmds_array [SQLCOM_END + 2][MAX_COMMAND_CHAR_NUMBERS] = {{0}};
static char whitelist_cmds_array [SQLCOM_END + 2][MAX_COMMAND_CHAR_NUMBERS] = {{0}};
static char record_cmds_array [SQLCOM_END + 2][MAX_COMMAND_CHAR_NUMBERS] = {{0}};
static char password_masking_cmds_array [SQLCOM_END + 2][MAX_COMMAND_CHAR_NUMBERS] = {{0}};
static char record_objs_array [MAX_NUM_OBJECT_ELEM + 2][MAX_OBJECT_CHAR_NUMBERS] = {{0}};
static char whitelist_users_array [MAX_NUM_USER_ELEM + 2][MAX_USER_CHAR_NUMBERS] = {{0}};
static bool record_empty_objs_set = true;
static int num_delay_cmds = 0;
static int num_whitelist_cmds = 0;
static int num_record_cmds = 0;
static int num_password_masking_cmds = 0;
static int num_record_objs = 0;
static int num_whitelist_users = 0;
static SHOW_VAR com_status_vars_array [MAX_COM_STATUS_VARS_RECORDS] = {{0}};
//regex stuff
static char password_masking_regex_check_buff[4096] = {0};
static char * password_masking_regex_string = NULL;
static char password_masking_regex_buff[4096] = {0};
//socket name
static char json_socket_name_buff[1024] = {0};
/**
* The trampoline functions we use. Will be set to point to allocated mem.
*/
static int (*trampoline_mysql_execute_command)(THD *thd) = NULL;
static unsigned int trampoline_mysql_execute_size =0;
#if MYSQL_VERSION_ID < 50600
static void (*trampoline_log_slow_statement)(THD *thd) = NULL;
static unsigned int trampoline_log_slow_statement_size =0;
#endif
#if MYSQL_VERSION_ID < 50505
static int (*trampoline_check_user)(THD *thd, enum enum_server_command command, const char *passwd, uint passwd_len, const char *db, bool check_count) = NULL;
static unsigned int trampoline_check_user_size =0;
#elif MYSQL_VERSION_ID < 50600
static bool (*trampoline_acl_authenticate)(THD *thd, uint connect_errors, uint com_change_user_pkt_len) = NULL;
static unsigned int trampoline_acl_authenticate_size =0;
#endif
static MYSQL_THDVAR_ULONG(is_thd_printed_list,
PLUGIN_VAR_READONLY | PLUGIN_VAR_NOSYSVAR | PLUGIN_VAR_NOCMDOPT, "avoid duplicate printing",
NULL, NULL,0,0,
#ifdef __x86_64__
0xffffffffffffff,
#else
0xffffffff,
#endif
1);
static MYSQL_THDVAR_ULONG(query_cache_table_list,
PLUGIN_VAR_READONLY | PLUGIN_VAR_NOSYSVAR | PLUGIN_VAR_NOCMDOPT, "Pointer to query cache table list.",
NULL, NULL,0,0,
#ifdef __x86_64__
0xffffffffffffff,
#else
0xffffffff,
#endif
1);
THDPRINTED * GetThdPrintedList (THD *thd)
{
THDPRINTED * pThdPrintedList= (THDPRINTED*)THDVAR(thd,is_thd_printed_list);
if (pThdPrintedList)
{
return pThdPrintedList;
}
THDVAR(thd,is_thd_printed_list) =0;
return NULL;
}
static int check_array(const char *cmds[],const char *array, int length) {
for (int k=0; array[k * length] !='\0';k++) {
for (int q = 0; cmds[q] != NULL; q++) {
const char *cmd = cmds[q];
int j = 0;
while (array[k * length + j] != '\0' && cmd[j] != '\0'
&& array[k * length + j] == tolower(cmd[j])) {
j++;
}
if (array[k * length + j] == '\0' && j != 0) {
return 1;
}
}
}
return 0;
}
//utility method checks if the passed db object is part of record_objs_array
static bool check_db_obj(const char * db, const char * name)
{
char db_obj[MAX_OBJECT_CHAR_NUMBERS] = {0};
char wildcard_obj[MAX_OBJECT_CHAR_NUMBERS] = {0};
char db_wildcard[MAX_OBJECT_CHAR_NUMBERS] = {0};
if(db && name &&
((strlen(db) + strlen(name)) < MAX_OBJECT_CHAR_NUMBERS - 2))
{
strcpy(db_obj, db);
strcat(db_obj, ".");
strcat(db_obj, name);
strcpy(wildcard_obj, "*.");
strcat(wildcard_obj, name);
strcpy(db_wildcard, db);
strcat(db_wildcard, ".*");
const char *objects[4];
objects[0] = db_obj;
objects[1] = wildcard_obj;
objects[2] = db_wildcard;
objects[3] = NULL;
return check_array(objects, (char *) record_objs_array, MAX_OBJECT_CHAR_NUMBERS);
}
return false;
}
//callback function returns if password masking is required according to cmd type
static my_bool check_do_password_masking(const char * cmd)
{
if(num_password_masking_cmds > 0)
{
const char *cmds[2];
cmds[0] = cmd;
cmds[1] = NULL;
return check_array(cmds, (const char *) password_masking_cmds_array, sizeof(password_masking_cmds_array[0]));
}
return false;
}
//utility compiles the regex. if fails zero outs password_masking_regex_string
static void password_masking_regex_compile()
{
int res = json_formatter.compile_password_masking_regex(password_masking_regex_string);
if(res)
{ password_masking_regex_buff[0] = '\0';
password_masking_regex_string = password_masking_regex_buff;
}
sql_print_information("%s Compile password_masking_regex res: [%d]", log_prefix, res);
}
static void audit(ThdSesData *pThdData)
{
THDPRINTED *pThdPrintedList = GetThdPrintedList (pThdData->getTHD());
if (num_whitelist_cmds > 0) {
const char * cmd = pThdData->getCmdName();
const char *cmds[2];
cmds[0] = cmd;
cmds[1] = NULL;
if (check_array(cmds, (char *) whitelist_cmds_array, MAX_COMMAND_CHAR_NUMBERS)) {
return;
}
}
if (num_whitelist_users > 0) {
const char * user = pThdData->getUserName(); //If name is present, then no need to log the query
const char *users[2];
if(NULL == user || '\0' == user[0]) //empty user use special symbol: "{}"
{
user = "{}";
}
users[0] = user;
users[1] = NULL;
if (check_array(users, (char *) whitelist_users_array, MAX_USER_CHAR_NUMBERS)) {
return;
}
}
bool do_objs_cmds_check = true;
if (force_record_logins_enable) {
const char * cmd = pThdData->getCmdName();
if (!strcasecmp(cmd, "Connect") || !strcasecmp(cmd, "Quit") || !strcasecmp(cmd, "Failed Login")) {
do_objs_cmds_check = false;
}
}
if (num_record_cmds > 0 && do_objs_cmds_check) {
const char * cmd = pThdData->getCmdName();
const char *cmds[2];
cmds[0] = cmd;
cmds[1] = NULL;
if (!check_array(cmds, (char *) record_cmds_array, MAX_COMMAND_CHAR_NUMBERS)) {
return;
}
}
if (num_record_objs > 0 && do_objs_cmds_check) {
bool matched = false;
if(pThdData->startGetObjects())
{
const char * db_name = NULL;
const char * obj_name = NULL;
while(!matched && pThdData->getNextObject(&db_name, &obj_name, NULL))
{
matched = check_db_obj(db_name, obj_name);
}
}
else //no objects
{
matched = record_empty_objs_set;
}
if (!matched) {
return;
}
}
if (pThdPrintedList && pThdPrintedList->cur_index < MAX_NUM_QUEUE_ELEM)
{
//audit the event if we haven't done so yet or in the case of prepare_sql we audit as the test "test select" doesn't go through mysql_execute_command
if (pThdPrintedList->is_thd_printed_queue[pThdPrintedList->cur_index] == 0 || strcmp(pThdData->getCmdName(), "prepare_sql") == 0)
{
Audit_handler::log_audit_all(pThdData);
pThdPrintedList->is_thd_printed_queue[pThdPrintedList->cur_index] = 1;
}
else //duplicate no need to audit then simply return
{
return;
}
}
else
{
Audit_handler::log_audit_all(pThdData);
}
if (delay_ms_val > 0)
{
const char * cmd = pThdData->getCmdName();
const char *cmds[2];
cmds[0] = cmd;
cmds[1] = NULL;
int delay = check_array(cmds, (char *) delay_cmds_array, MAX_COMMAND_CHAR_NUMBERS);
if (delay)
{
//Audit_file_handler::print_sleep(thd,delay_ms_val);
my_sleep (delay_ms_val *1000);
}
}
}
static int (*trampoline_send_result_to_client)(Query_cache *pthis, THD *thd, char *sql, uint query_length) = NULL;
#if MYSQL_VERSION_ID > 50505
static bool (*trampoline_open_tables)(THD *thd, TABLE_LIST **start, uint *counter, uint flags,
Prelocking_strategy *prelocking_strategy) = NULL;
#else
static int (*trampoline_open_tables)(THD *thd, TABLE_LIST **start, uint *counter, uint flags) = NULL;
#endif
QueryTableInf * Audit_formatter::getQueryCacheTableList1 (THD *thd)
{
return (QueryTableInf*) THDVAR(thd, query_cache_table_list);
}
static bool (*trampoline_check_table_access)(THD *thd, ulong want_access,TABLE_LIST *tables, uint number, bool no_errors) = NULL;
static bool audit_check_table_access(THD *thd, ulong want_access,TABLE_LIST *tables,
uint number, bool no_errors)
{
TABLE_LIST *pTables;
bool res = trampoline_check_table_access (thd, want_access, tables, number, no_errors);
if (!res && tables)
{
pTables = tables;
QueryTableInf * pQueryTableInf =(QueryTableInf*) THDVAR (thd,query_cache_table_list);
if (pQueryTableInf)
{
while (pTables)
{
if (pQueryTableInf->num_of_elem < MAX_NUM_QUERY_TABLE_ELEM && pQueryTableInf->num_of_elem>=0)
{
pQueryTableInf->db[pQueryTableInf->num_of_elem] = (char*) thd_alloc (thd, strlen(Audit_formatter::table_get_db_name(pTables))+1);
strcpy (pQueryTableInf->db[pQueryTableInf->num_of_elem],Audit_formatter::table_get_db_name(pTables));
pQueryTableInf->table_name[pQueryTableInf->num_of_elem] = (char*) thd_alloc (thd, strlen(Audit_formatter::table_get_name(pTables)) +1);
strcpy (pQueryTableInf->table_name[pQueryTableInf->num_of_elem],Audit_formatter::table_get_name(pTables));
pQueryTableInf->object_type[pQueryTableInf->num_of_elem] = Audit_formatter::retrieve_object_type ( pTables);
pQueryTableInf->num_of_elem ++;
}
pTables = pTables->next_global;
}
}
}
return res;
}
static unsigned int trampoline_check_table_access_size = 0;
static int audit_send_result_to_client(Query_cache *pthis, THD *thd, char *sql, uint query_length)
{
int res;
void *pList = thd_alloc (thd, sizeof (QueryTableInf));
if (pList)
{
memset (pList,0,sizeof (QueryTableInf));
THDVAR(thd, query_cache_table_list) =(ulong)pList;
}
res = trampoline_send_result_to_client (pthis,thd, sql, query_length);
if (res)
{
ThdSesData thd_data (thd);
audit (&thd_data);
}
THDVAR(thd, query_cache_table_list) = 0;
return res;
}
static unsigned int trampoline_send_result_to_client_size =0;
#if MYSQL_VERSION_ID > 50505
static bool audit_open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags,
Prelocking_strategy *prelocking_strategy)
{
bool res;
res = trampoline_open_tables (thd, start, counter, flags, prelocking_strategy);
//only log if thread id or query id is non 0 (otherwise this is comming from startup activity)
if(Audit_formatter::thd_inst_thread_id(thd) || Audit_formatter::thd_inst_query_id(thd))
{
ThdSesData thd_data (thd);
audit(&thd_data);
}
return res;
}
#else
static int audit_open_tables(THD *thd, TABLE_LIST **start, uint *counter, uint flags)
{
bool res;
res = trampoline_open_tables (thd, start, counter, flags);
//only log if thread id or query id is non 0 (otherwise this is comming from startup activity)
if(Audit_formatter::thd_inst_thread_id(thd) || Audit_formatter::thd_inst_query_id(thd))
{
ThdSesData thd_data (thd);
audit(&thd_data);
}
return res;
}
#endif
static unsigned int trampoline_open_tables_size =0;
//called by log_slow_statement and general audit event caught by audit interface
static void audit_post_execute(THD * thd)
{
//only audit non query events
//query events are audited by mysql execute command
if (Audit_formatter::thd_inst_command(thd) != COM_QUERY)
{
ThdSesData ThdData (thd);
if (strcasestr (ThdData.getCmdName(), "show_fields")==NULL)
{
audit(&ThdData);
}
}
}
/*
Plugin descriptor
*/
//in 5.6 we use the AUDIT plugin interface. In 5.1/5.5 we just use the general DAEMON plugin
#if MYSQL_VERSION_ID < 50600
static int plugin_type = MYSQL_DAEMON_PLUGIN;
static struct st_mysql_daemon audit_plugin =
{ MYSQL_DAEMON_INTERFACE_VERSION };
#else
static void audit_notify(THD *thd, unsigned int event_class,
const void * event)
{
if (MYSQL_AUDIT_GENERAL_CLASS == event_class)
{
const struct mysql_event_general *event_general =
(const struct mysql_event_general *) event;
if(MYSQL_AUDIT_GENERAL_STATUS == event_general->event_subclass)
{
audit_post_execute(thd);
}
}
else if (MYSQL_AUDIT_CONNECTION_CLASS == event_class)
{
const struct mysql_event_connection *event_connection =
(const struct mysql_event_connection *) event;
//only audit for connect and change_user. disconnect is caught by general event
if(event_connection->event_subclass != MYSQL_AUDIT_CONNECTION_DISCONNECT)
{
ThdSesData ThdData (thd);
audit (&ThdData);
}
}
}
static int plugin_type = MYSQL_AUDIT_PLUGIN;
static struct st_mysql_audit audit_plugin=
{
MYSQL_AUDIT_INTERFACE_VERSION, /* interface version */
NULL, /* release_thd function */
audit_notify, /* notify function */
{ (unsigned long) MYSQL_AUDIT_GENERAL_CLASSMASK |
MYSQL_AUDIT_CONNECTION_CLASSMASK } /* class mask */
};
#endif
//some extern definitions which are not in include files
extern void log_slow_statement(THD *thd);
extern int mysql_execute_command(THD *thd);
#if MYSQL_VERSION_ID >= 50505
//in 5.5 builtins is named differently
#define mysqld_builtins mysql_mandatory_plugins
#endif
extern struct st_mysql_plugin *mysqld_builtins[];
void remove_hot_functions ()
{
void * target_function = NULL;
#if MYSQL_VERSION_ID < 50600
target_function = (void *) log_slow_statement;
remove_hot_patch_function(target_function,
(void*) trampoline_log_slow_statement, trampoline_log_slow_statement_size, true);
trampoline_log_slow_statement_size=0;
#endif
#if MYSQL_VERSION_ID < 50505
target_function = (void *) check_user;
remove_hot_patch_function(target_function,
(void*) trampoline_check_user, trampoline_check_user_size, true);
trampoline_check_user_size=0;
#elif MYSQL_VERSION_ID < 50600
target_function = (void *) acl_authenticate;
remove_hot_patch_function(target_function,
(void*) trampoline_acl_authenticate, trampoline_acl_authenticate_size, true);
trampoline_acl_authenticate_size=0;
#endif
#if MYSQL_VERSION_ID > 50505
target_function = (void *)*(bool (*)(THD *thd, TABLE_LIST **start, uint *counter, uint flags,
Prelocking_strategy *prelocking_strategy)) &open_tables;
remove_hot_patch_function(target_function,
(void*) trampoline_open_tables, trampoline_open_tables_size, true);
trampoline_open_tables_size=0;
#else
target_function = (void *)*(int (*)(THD *thd, TABLE_LIST **start, uint *counter, uint flags)) &open_tables;
remove_hot_patch_function(target_function,
(void*) trampoline_open_tables, trampoline_open_tables_size, true);
trampoline_open_tables_size=0;
#endif
int (Query_cache::*pf_send_result_to_client)(THD *,char *, uint) = &Query_cache::send_result_to_client;
target_function = *(void **) &pf_send_result_to_client;
remove_hot_patch_function(target_function,
(void*) trampoline_send_result_to_client, trampoline_send_result_to_client_size, true);
trampoline_send_result_to_client_size=0;
remove_hot_patch_function((void*) check_table_access,
(void*) trampoline_check_table_access,
trampoline_check_table_access_size, true);
trampoline_check_table_access_size=0;
remove_hot_patch_function((void*)mysql_execute_command,
(void*) trampoline_mysql_execute_command,
trampoline_mysql_execute_size, true);
trampoline_mysql_execute_size=0;
}
int is_remove_patches (ThdSesData *pThdData)
{
static bool called_once = false;
const char *cmd = pThdData->getCmdName();
const char *sUninstallPlugin = "uninstall_plugin";
LEX *pLex = Audit_formatter::thd_lex(pThdData->getTHD());
if (pThdData->getTHD() && pLex!=NULL && strncasecmp (cmd,sUninstallPlugin ,strlen (sUninstallPlugin))==0 )
{
LEX_STRING Lex_comment = *(LEX_STRING*)(((unsigned char *) pLex) + Audit_formatter::thd_offsets.lex_comment);
if (strncasecmp(Lex_comment.str, "AUDIT", 5) == 0)
{
if (!uninstall_plugin_enable)
{
my_message (ER_NOT_ALLOWED_COMMAND,"Uninstall AUDIT plugin disabled",MYF(0));
return 2;
}
Audit_handler::stop_all();
remove_hot_functions ();
if(!called_once)
{
called_once = true;
my_message (WARN_PLUGIN_BUSY,"Uninstall AUDIT plugin must be called again to complete",MYF(0));
return 2;
}
return 1;
}
}
return 0;
}
/*
* Over ride functions for hot patch + audit. We call our audit function
* after the execute command so all tables are resolved.
*/
static int audit_mysql_execute_command(THD *thd)
{
bool firstTime = false;
THDPRINTED *pThdPrintedList = GetThdPrintedList (thd);
if (pThdPrintedList)
{
if (pThdPrintedList->cur_index < (MAX_NUM_QUEUE_ELEM -1) )
{
pThdPrintedList->cur_index ++;
pThdPrintedList->is_thd_printed_queue[pThdPrintedList->cur_index] =0;
}
}
else
{
firstTime = true;
pThdPrintedList = (THDPRINTED *) thd_alloc (thd, sizeof (THDPRINTED));
if (pThdPrintedList)
{
memset (pThdPrintedList, 0, sizeof (THDPRINTED));
//pThdPrintedList->cur_index = 0;
THDVAR(thd,is_thd_printed_list) = (ulong) pThdPrintedList;
}
}
ThdSesData thd_data (thd);
const char *cmd = thd_data.getCmdName();
if (strcasestr (cmd,"alter") !=NULL || strcasestr (cmd,"drop") !=NULL || strcasestr (cmd, "create") !=NULL || strcasestr (cmd, "truncate") !=NULL || strcasestr (cmd, "rename") !=NULL)
{
audit(&thd_data);
}
int res;
#if defined(MARIADB_BASE_VERSION)
if(Audit_formatter::thd_killed(thd) >= KILL_CONNECTION)
#else
if(Audit_formatter::thd_killed(thd) == THD::KILL_CONNECTION)
#endif
{
res = 1;
}
else
{
switch (is_remove_patches(&thd_data))
{
case 1:
//hot patch function were removed and we call the real execute (restored)
res = mysql_execute_command(thd);
break;
case 2:
//denied uninstall plugin
res = 1;
break;
default:
//everything else
res = trampoline_mysql_execute_command(thd);
}
}
audit(&thd_data);
if (pThdPrintedList && pThdPrintedList->cur_index >0)
{
pThdPrintedList->cur_index --;
}
if(firstTime)
{
THDVAR(thd,is_thd_printed_list) = 0;
}
return res;
}
#if MYSQL_VERSION_ID < 50600
static void audit_log_slow_statement(THD * thd)
{
trampoline_log_slow_statement(thd);
audit_post_execute(thd);
}
#endif
#if MYSQL_VERSION_ID < 50505
static int audit_check_user(THD *thd, enum enum_server_command command,
const char *passwd, uint passwd_len, const char *db,
bool check_count)
{
int res = trampoline_check_user (thd, command, passwd, passwd_len, db, check_count);
ThdSesData ThdData (thd);
audit (&ThdData);
return (res);
}
#elif MYSQL_VERSION_ID < 50600
//only for 5.5
//in 5.6: we use audit plugin event to get the login event
static bool audit_acl_authenticate(THD *thd, uint connect_errors, uint com_change_user_pkt_len)
{
bool res = trampoline_acl_authenticate (thd, connect_errors, com_change_user_pkt_len);
ThdSesData ThdData (thd);
audit (&ThdData);
return (res);
}
#endif
static bool parse_thd_offsets_string (char *poffsets_string)
{
char offset_str [2048] = {0};
char *poffset_str = offset_str;
strncpy (poffset_str,poffsets_string,2048);
char * comma_delimiter = strchr (poffset_str,',');
size_t i =0;
OFFSET *pOffset;
size_t len = strlen (poffset_str);
for (size_t j=0;j<len;j++)
{
if (!((poffset_str[j] >= '0' && poffset_str[j] <='9') || poffset_str[j] == ' ' || poffset_str[j] == ','))
return false;
}
while (comma_delimiter !=NULL)
{
*comma_delimiter = '\0';
pOffset = (OFFSET*)&Audit_formatter::thd_offsets.query_id + i;
if ((size_t)pOffset- (size_t)&Audit_formatter::thd_offsets < sizeof (Audit_formatter::thd_offsets))
{
sscanf (poffset_str, "%zu", pOffset);
}
else
{
return false;
}
i++;
poffset_str = comma_delimiter + 1;
comma_delimiter = strchr (poffset_str,',');
}
if (poffset_str !=NULL)
{
pOffset = &Audit_formatter::thd_offsets.query_id + i;
if ((size_t)pOffset- (size_t)&Audit_formatter::thd_offsets < sizeof (Audit_formatter::thd_offsets))
{
sscanf (poffset_str, "%zu", pOffset);
}
else
{
return false;
}
}
return true;
}
static bool validate_offsets(const ThdOffsets * offset)
{
//check that offsets are actually correct. We use a buff of memory as a dummy THD (32K is high enough)
char buf[32*1024] = {0};
THD * thd = (THD *)buf;
//sanity check that offsets match
//we set the thread id to a value using the offset and then check that the value matches what thd_get_thread_id returns
const my_thread_id thread_id_test_val = 123456;
(*(my_thread_id *) (((char *) thd)+ offset->thread_id)) = thread_id_test_val;
my_thread_id res= thd_get_thread_id(thd);
if (res != thread_id_test_val)
{
sql_print_error(
"%s Offsets: %s (%s) match thread validation check fails with value: %lu. Skipping offest.",
log_prefix, offset->version, offset->md5digest, res);
return false;
}
//extended validation via security_context method
//can be disabled via: audit_validate_offsets_extended=OFF
if(validate_offsets_extended_enable)
{
const query_id_t query_id_test_val = 789;
(*(query_id_t *) (((char *) thd)+ offset->query_id)) = query_id_test_val;
Security_context * sctx = (Security_context *) (((unsigned char *) thd) + offset->main_security_ctx);
char user_test_val[] = "aud_tusr";
if(!offset->sec_ctx_user) //use compiled header
{
sctx->user = user_test_val;
}
else
{
(*(const char **) (((unsigned char *) sctx) + offset->sec_ctx_user)) = user_test_val;
}
char buffer[2048] = {0};
thd_security_context(thd, buffer, 2048, 1000);
//verfiy our buffer contains query id
if(strstr(buffer, " 789") == NULL || strstr(buffer, user_test_val) == NULL)
{
sql_print_error(
"%s Offsets: %s (%s) sec context validation check fails with value: %s. Skipping offest.",
log_prefix, offset->version, offset->md5digest, buffer);
return false;
}
sql_print_information(
"%s extended offsets validate res: %s", log_prefix, buffer);
}
return true;
}
/**
* Calculate md5 sum of a file.
*
* @file_name: file to calc md5 for
* @digest_str: string to fill with digest result should be big enought to hold 32 chars
*
* @return true on success.
*/
static bool calc_file_md5(const char * file_name, char * digest_str)
{
File fd;
unsigned char digest[16] = {0};
bool ret = false;
if ((fd = my_open(file_name, O_RDONLY, MYF(MY_WME))) < 0)
{
sql_print_error("%s Failed file open: [%s], errno: %d.",
log_prefix, file_name, errno);
return false;
}
my_MD5Context context;
my_MD5Init(&context);
const size_t buff_size = 16384;
unsigned char file_buff[buff_size] = {0};
ssize_t res;
do
{
res = read(fd, file_buff, buff_size);
if(res > 0)
{
my_MD5Update(&context, file_buff, res);
}
}
while(res > 0);
if(res == 0) //reached end of file
{
my_MD5Final(digest, &context);
ret = true;
}
else
{
sql_print_error("%s Failed program read. res: %zd, errno: %d.",
log_prefix, res, errno);
}
(void) my_close(fd, MYF(0));
if(ret) //we got the digest
{
for (int j = 0; j < 16; j++)
{
sprintf(&(digest_str[j * 2]), "%02x", digest[j]);
}
}
return ret;
}
/**
* Setup the offsets needs to extract data from THD.
*
* return 0 on success otherwise 1
*/
static int setup_offsets()
{
DBUG_ENTER("setup_offsets");
sql_print_information ("%s setup_offsets audit_offsets: %s validate_checksum: %d offsets_by_version: %d",
log_prefix, offsets_string, validate_checksum_enable, offsets_by_version_enable);
char digest_str [128] = {0};
const ThdOffsets * offset;
//setup digest_str to contain the md5sum in hex
calc_file_md5(my_progname, digest_str);
sql_print_information(
"%s mysqld: %s (%s) ", log_prefix, my_progname, digest_str);
//if present in my.cnf
//[mysqld]
//audit_validate_checksum=1
// or if
//audit_checksum=0f4d7e3b17eb36f17aafe4360993a769
//if (validate_checksum_enable || (checksum_string != NULL && strlen(checksum_string) > 0))
//{
//if present the offset_string specified in my.cnf
//[mysqld]
//audit_offsets=6200, 6264, 3672, 3944, 88, 2048
if (offsets_string != NULL)
{
if (checksum_string != NULL && strlen(checksum_string) > 0)
{
if (strncasecmp(checksum_string, digest_str, 32))
{
sql_print_information(
"%s checksum check failed for %s, but found %s",
log_prefix, checksum_string, digest_str);
DBUG_RETURN(1);
}
}
if (parse_thd_offsets_string (offsets_string))
{
sql_print_information ("%s setup_offsets Audit_formatter::thd_offsets values: %zu %zu %zu %zu %zu %zu %zu %zu %zu %zu", log_prefix,
Audit_formatter::thd_offsets.query_id,
Audit_formatter::thd_offsets.thread_id,
Audit_formatter::thd_offsets.main_security_ctx,
Audit_formatter::thd_offsets.command,
Audit_formatter::thd_offsets.lex,
Audit_formatter::thd_offsets.lex_comment,
Audit_formatter::thd_offsets.sec_ctx_user,
Audit_formatter::thd_offsets.sec_ctx_host,
Audit_formatter::thd_offsets.sec_ctx_ip,
Audit_formatter::thd_offsets.sec_ctx_priv_user);
if (!validate_offsets(&Audit_formatter::thd_offsets))
{
sql_print_error("%s Offsets set didn't pass validation. audit_offsets: %s .", log_prefix, offsets_string);
DBUG_RETURN(1);
}
}
else
{
sql_print_error("%s Failed parsing audit_offsets: %s", log_prefix, offsets_string);
DBUG_RETURN(1);
}
sql_print_information ("%s Validation passed. Using offsets from audit_offsets: %s",log_prefix, offsets_string);
DBUG_RETURN(0);
//exit from function
}
size_t arr_size = thd_offsets_arr_size;
//iterate and search for the first offset which matches our checksum
if(validate_checksum_enable && strlen(digest_str) > 0)
{
for(size_t i=0; i < arr_size; i++)
{
offset = thd_offsets_arr + i;
if (strlen(offset->md5digest) >0)
{
if (!strncasecmp(digest_str, offset->md5digest, 32))
{
sql_print_information("%s Checksum verified. Using offsets from offset version: %s (%s)", log_prefix, offset->version, digest_str);
Audit_formatter::thd_offsets = *offset;
DBUG_RETURN(0);
//return
}
}
}
}
if(offsets_by_version_enable)
{
bool server_is_ndb = strstr(server_version, "ndb") != NULL;
for(size_t i=0; i < arr_size; i++)
{
offset = thd_offsets_arr + i;
const char * version = offset->version;
bool version_is_ndb = strstr(offset->version, "ndb") != NULL;
const char * dash = strchr(version, '-');
char version_stripped[16] = {0};
if(dash) //we use the version string up to the '-'
{
size_t tocopy = dash - version;
if(tocopy > 15) tocopy = 15; //sanity
strncpy(version_stripped, version, tocopy);
version = version_stripped;
}
if(strstr(server_version, version))
{
if(server_is_ndb == version_is_ndb)
{
if (validate_offsets(offset))
{
sql_print_information("%s Using offsets from offset version: %s (%s)", log_prefix, offset->version, offset->md5digest);
Audit_formatter::thd_offsets = *offset;
DBUG_RETURN(0);
}
else
{
//try doing 24 byte decrement on THD offsets. Seen that on Ubuntu/Debian this is valid. On 5.6 this is 16 bytes.
#if MYSQL_VERSION_ID < 50600
OFFSET dec = 24;
#else
OFFSET dec = 16;
#endif
ThdOffsets decoffsets = *offset;
decoffsets.query_id -= dec;
decoffsets.thread_id -= dec;
decoffsets.main_security_ctx -= dec;
decoffsets.command -= dec;
if (validate_offsets(&decoffsets))
{
Audit_formatter::thd_offsets = decoffsets;
sql_print_information("%s Using decrement (%zu) offsets from offset version: %s (%s) values: %zu %zu %zu %zu %zu %zu %zu %zu %zu %zu",
log_prefix, dec, offset->version, offset->md5digest,
Audit_formatter::thd_offsets.query_id,
Audit_formatter::thd_offsets.thread_id,
Audit_formatter::thd_offsets.main_security_ctx,
Audit_formatter::thd_offsets.command,
Audit_formatter::thd_offsets.lex,
Audit_formatter::thd_offsets.lex_comment,
Audit_formatter::thd_offsets.sec_ctx_user,
Audit_formatter::thd_offsets.sec_ctx_host,
Audit_formatter::thd_offsets.sec_ctx_ip,
Audit_formatter::thd_offsets.sec_ctx_priv_user);
DBUG_RETURN(0);
}
}
}//ndb check
#if defined(__x86_64__) && MYSQL_VERSION_ID > 50505
else if(server_is_ndb)
{
//in 64bit 5.5 we've seen ndb has an offset of 32 on first 2 values
OFFSET inc = 32;
ThdOffsets incoffsets = *offset;
incoffsets.query_id += inc;
incoffsets.thread_id += inc;
if (validate_offsets(&incoffsets))
{
Audit_formatter::thd_offsets = incoffsets;
sql_print_information("%s Using increment (%zu) offsets from offset version: %s (%s) values: %zu %zu %zu %zu %zu %zu %zu %zu %zu %zu",
log_prefix, inc, offset->version, offset->md5digest,
Audit_formatter::thd_offsets.query_id,
Audit_formatter::thd_offsets.thread_id,
Audit_formatter::thd_offsets.main_security_ctx,
Audit_formatter::thd_offsets.command,
Audit_formatter::thd_offsets.lex,
Audit_formatter::thd_offsets.lex_comment,
Audit_formatter::thd_offsets.sec_ctx_user,
Audit_formatter::thd_offsets.sec_ctx_host,
Audit_formatter::thd_offsets.sec_ctx_ip,
Audit_formatter::thd_offsets.sec_ctx_priv_user);
DBUG_RETURN(0);
}
}
#endif
}
}
}
sql_print_information("%s Couldn't find proper THD offsets for: %s", log_prefix, server_version);
DBUG_RETURN(1);
}
const char * retrieve_command (THD * thd, bool & is_sql_cmd)
{
const char *cmd = NULL;
is_sql_cmd = false;
int command = Audit_formatter::thd_inst_command(thd);
if (command < 0 || command > COM_END)
{
command = COM_END;
}
//check if from query cache. If so set to select and return
if(THDVAR(thd, query_cache_table_list) != 0)
{
return "select";
}
const int sql_command = thd_sql_command(thd);
if (sql_command >=0 && sql_command < MAX_COM_STATUS_VARS_RECORDS )
{
is_sql_cmd = true;
cmd = com_status_vars_array[sql_command].name;
}
if(!cmd)
{
cmd = command_name[command].str;
}
const char * user = Audit_formatter::thd_inst_main_security_ctx_user(thd);
const char * priv_user = Audit_formatter::thd_inst_main_security_ctx_priv_user(thd);
if (strcmp (cmd, "Connect") ==0 && ((user && strcmp(user, "event_scheduler") != 0) && (priv_user == NULL || *priv_user == 0x0)))
{
cmd = "Failed Login";
}
return cmd;
}
static int set_com_status_vars_array ()
{
DBUG_ENTER("set_com_status_vars_array");
SHOW_VAR *com_status_vars;
int sv_idx =0;
while (strcmp (status_vars[sv_idx].name,"Com") !=0 && status_vars[sv_idx].name != NullS)
{
sv_idx ++;
}
if (strcmp (status_vars[sv_idx].name,"Com")==0)
{
com_status_vars = (SHOW_VAR*)status_vars[sv_idx].value;
int status_vars_index =0;
//we use "select" as 0 offset (SQLCOM_SELECT=0)
while(strcmp(com_status_vars[status_vars_index].name,"select") !=0 && com_status_vars[status_vars_index].name != NullS)
{
status_vars_index ++;
}
if(strcmp(com_status_vars[status_vars_index].name,"select") !=0)
{
sql_print_error("%s Failed finding 'select' index in com_status_vars: [%p]. Plugin Init failed.",
log_prefix, com_status_vars);
DBUG_RETURN (1);
}
size_t initial_offset = (size_t) com_status_vars[status_vars_index].value;
status_vars_index =0;
while (com_status_vars[status_vars_index].name != NullS)
{
int sql_command_idx = (com_status_vars[status_vars_index].value - (char*) (initial_offset)) / sizeof (ulong);
if (sql_command_idx >=0 && sql_command_idx < MAX_COM_STATUS_VARS_RECORDS)
{
com_status_vars_array [sql_command_idx].name = com_status_vars[status_vars_index].name;
com_status_vars_array [sql_command_idx].type = com_status_vars[status_vars_index].type;
com_status_vars_array [sql_command_idx].value = com_status_vars[status_vars_index].value;
}
status_vars_index ++;
}
sql_print_information("%s Done initializing sql command names. status_vars_index: [%d], com_status_vars: [%p].",
log_prefix, status_vars_index, com_status_vars);
}
else
{
sql_print_error("%s Failed looking up 'Com' entry in status_vars. Plugin Init failed.",
log_prefix);
DBUG_RETURN (1);
}
DBUG_RETURN (0);
}
static int string_to_array(const void *save, void *array,
int rows, int length)
{
const char* save_string;
save_string = *static_cast<const char* const *> (save);
char* string_array;
string_array = (char *) array;
int r = 0;
if (save_string != NULL)
{
int p = 0;
int last_char=0; //points to the index after last non-whitespace char (used to trim whitespace at end)
for (int i = 0; save_string[i] != '\0'; i++)
{
if (0 == p && isspace(save_string[i]))
{
//leading white space. trim it out
continue;
}
if (save_string[i] == ',')
{
if (p > 0)
{
string_array[r * length + last_char] = '\0';
last_char = 0;
p = 0;
r++;
if (r == (rows - 1))
{
break;
}
}
}
else
{
string_array[r * length + p] = tolower(save_string[i]);
p++;
if(!isspace(save_string[i]))
{
last_char = p;
}
}
}
if (last_char > 0)
{
string_array[r * length + last_char] = '\0';
r++;
}
string_array[r * length + 0] = '\0';
}
return r;
}
__attribute__ ((noinline)) static void trampoline_dummy_func_for_mem()
{
TRAMPOLINE_NOP_DEF
}
//holds memory used for trampoline
static void * trampoline_mem = NULL;
//pointer to current free mem
static void * trampoline_mem_free = NULL;
/**
* Utility method for hot patching
*/
static int do_hot_patch(void ** trampoline_func_pp, unsigned int * trampoline_size,
void* target_function, void* audit_function, const char * func_name)
{
//16 byte align the pointer
DATATYPE_ADDRESS addrs = (DATATYPE_ADDRESS)trampoline_mem_free + 15;
*trampoline_func_pp = (void*)(addrs & ~0x0F);
//hot patch functions
int res = hot_patch_function(target_function, audit_function,
*trampoline_func_pp, trampoline_size, true);
if (res != 0)
{
//hot patch failed.
sql_print_error("%s unable to hot patch %s (%p). res: %d.",
log_prefix, func_name, target_function, res);
return 1;
}
sql_print_information(
"%s hot patch for: %s (%p) complete. Audit func: %p, Trampoline address: %p size: %u.",
log_prefix, func_name, target_function, audit_function, *trampoline_func_pp, *trampoline_size);
trampoline_mem_free = (void *)(((DATATYPE_ADDRESS)*trampoline_func_pp) + *trampoline_size + jump_size());
return 0;
}
#define DECLARE_STRING_ARR_UPDATE_FUNC(NAME) \
static void NAME ## _string_update(THD *thd, struct st_mysql_sys_var *var, void *tgt, const void *save)\
{\
num_ ## NAME = string_to_array(save, NAME ## _array, array_elements( NAME ## _array), sizeof( NAME ## _array[0]));\
strncpy( NAME ##_buff , *static_cast<char*const*>(save), array_elements( NAME ## _buff) - 1);\
NAME ## _string = NAME ##_buff;\
sql_print_information("%s Set " #NAME " num: %d, value: %s", log_prefix, num_ ## NAME, NAME ## _string);\
}
DECLARE_STRING_ARR_UPDATE_FUNC(delay_cmds)
DECLARE_STRING_ARR_UPDATE_FUNC(whitelist_cmds)
DECLARE_STRING_ARR_UPDATE_FUNC(record_cmds)
DECLARE_STRING_ARR_UPDATE_FUNC(password_masking_cmds)
DECLARE_STRING_ARR_UPDATE_FUNC(whitelist_users)
DECLARE_STRING_ARR_UPDATE_FUNC(record_objs)
static void password_masking_regex_string_update(THD *thd, struct st_mysql_sys_var *var, void *tgt, const void *save)
{
const char * str_val = *static_cast<char*const*>(save);
const char * str = str_val;
//copy str to buffer only if str is not pointing to buff
if(str != password_masking_regex_buff)
{
strncpy( password_masking_regex_buff , str, array_elements( password_masking_regex_buff) - 1);
}
password_masking_regex_string = password_masking_regex_buff;
password_masking_regex_compile();
sql_print_information("%s Set password_masking_regex value: [%s]", log_prefix, str_val);
}
static void replace_char(char * str, const char tofind, const char rplc)
{
size_t n = strlen(str);
for(size_t i = 0; i< n; i++)
{
if(tofind == str[i])
{
str[i] = rplc;
}
}
}
static void json_socket_name_update(THD *thd, struct st_mysql_sys_var *var, void *tgt, const void *save)
{
const char * str_val = *static_cast<char*const*>(save);
const char * str = str_val;
const size_t buff_len = array_elements( json_socket_name_buff) -1;
//copy str to buffer only if str is not pointing to buff
if(NULL == str)
{
json_socket_name_buff[0] = '\0';
}
else if(str != json_socket_name_buff)
{
strncpy( json_socket_name_buff , str, buff_len);
}
if(strlen(json_socket_name_buff) == 0 && (mysqld_port > 0 || mysqld_unix_port)) //set default
{
const char * name_prefix = "/tmp/mysql.audit_";
size_t indx = strlen(name_prefix); //count how much to move forward the buff
strncpy( json_socket_name_buff, name_prefix, buff_len);
char cwd_buff[512] = {0};
my_getwd(cwd_buff, array_elements(cwd_buff) - 1, 0);
replace_char(cwd_buff, '/', '_');
size_t cwd_len = strlen(cwd_buff);
if(cwd_len > 0 && '_' != cwd_buff[cwd_len-1]) //add _ to end
{
strncpy(cwd_buff + cwd_len, "_", array_elements(cwd_buff) - 1 - cwd_len);
}
strncpy(json_socket_name_buff + indx, cwd_buff, buff_len - indx);
indx += cwd_len;
if(indx < buff_len)
{
if(mysqld_port > 0)
{
snprintf(json_socket_name_buff + indx, buff_len - indx, "%u", mysqld_port);
}
else
{
strncpy(json_socket_name_buff + indx, mysqld_unix_port, buff_len - indx);
replace_char(json_socket_name_buff + indx, '/', '_');
}
}
else //should never happen
{
sql_print_error("%s json_socket_name_buff not big enough to set default name. buff: %s",
log_prefix, json_socket_name_buff);
}
}
json_socket_handler.m_io_dest = json_socket_name_buff;
sql_print_information("%s Set json_socket_name str: [%s] value: [%s]", log_prefix, str, json_socket_handler.m_io_dest);
}
//check that the regex compiles. Return 0 on success.
static int password_masking_regex_check(THD* thd, struct st_mysql_sys_var * var, void* save, st_mysql_value* value)
{
int length= array_elements(password_masking_regex_check_buff);
const char * str = value->val_str(value, password_masking_regex_check_buff, &length);
*(const char**)save= str;
if(NULL == str || str[0] == '\0') //empty string is fine (means disabled)
{
return 0;
}
int res = 1;
pcre * preg = Audit_json_formatter::regex_compile(str);
if(preg)
{
res = 0;
}
pcre_free(preg);
return res;
}
//extended method to set also record_empty_objs_set
static void record_objs_string_update_extended(THD *thd, struct st_mysql_sys_var *var, void *tgt, const void *save)
{
record_objs_string_update(thd, var, tgt, save);
if(num_record_objs > 0) //check if to record also the empty set of objects
{
const char *objects[] = {"{}", NULL};
record_empty_objs_set = check_array(objects, (const char *) record_objs_array, sizeof(record_objs_array[0]));
}
else
{
record_empty_objs_set = true;
}
sql_print_information("%s Set record_empty_objs: %d", log_prefix, record_empty_objs_set);
}
/*
Initialize the plugin installation.
SYNOPSIS
audit_plugin_init()
RETURN VALUE
0 success
1 failure
*/
static int audit_plugin_init(void *p)
{
DBUG_ENTER("audit_plugin_init");
#ifdef __x86_64__
const char * arch = "64bit";
#else
const char * arch = "32bit";
#endif
int interface_ver = audit_plugin.interface_version ;
#if MYSQL_VERSION_ID < 50600
interface_ver = interface_ver >> 8;
#endif
sql_print_information(
"%s starting up. Version: %s , Revision: %s (%s). AUDIT plugin interface version: %d (0x%x). MySQL Server version: %s.",
log_prefix, MYSQL_AUDIT_PLUGIN_VERSION,
MYSQL_AUDIT_PLUGIN_REVISION, arch, interface_ver, interface_ver,
server_version);
//setup our offsets.
if(setup_offsets() != 0)
{
DBUG_RETURN(1);
}
if (delay_cmds_string != NULL) {
delay_cmds_string_update(NULL, NULL, NULL, &delay_cmds_string);
}
if (whitelist_cmds_string != NULL) {
whitelist_cmds_string_update(NULL, NULL, NULL, &whitelist_cmds_string);
}
if (record_cmds_string != NULL) {
record_cmds_string_update(NULL, NULL, NULL, &record_cmds_string);
}
if (whitelist_users_string != NULL) {
whitelist_users_string_update(NULL, NULL, NULL, &whitelist_users_string);
}
if (record_objs_string != NULL) {
record_objs_string_update_extended(NULL, NULL, NULL, &record_objs_string);
}
if (NULL != password_masking_cmds_string) {
password_masking_cmds_string_update(NULL, NULL, NULL, &password_masking_cmds_string);
}
if (NULL != password_masking_regex_string) {
password_masking_regex_string_update(NULL, NULL, NULL, &password_masking_regex_string);
}
//update to generate the default if needed
json_socket_name_update(NULL, NULL, NULL, &(json_socket_handler.m_io_dest));
//set the password masking callback for json formatters
json_formatter.m_perform_password_masking = check_do_password_masking;
//setup audit handlers (initially disabled)
int res = json_file_handler.init(&json_formatter);
if (res != 0)
{
sql_print_error(
"%s unable to init json file handler. res: %d. Aborting.",
log_prefix, res);
DBUG_RETURN(1);
}
res = json_socket_handler.init(&json_formatter);
if (res != 0)
{
sql_print_error(
"%s unable to init json socket handler. res: %d. Aborting.",
log_prefix, res);
DBUG_RETURN(1);
}
//enable according to what we have in *file_handler_enable (this is set accordingly by sysvar functionality)
json_file_handler.set_enable(json_file_handler_enable);
json_socket_handler.set_enable(json_socket_handler_enable);
Audit_handler::m_audit_handler_list[Audit_handler::JSON_FILE_HANDLER]
= &json_file_handler;
Audit_handler::m_audit_handler_list[Audit_handler::JSON_SOCKET_HANDLER]
= &json_socket_handler;
//align our trampoline mem on its own page
const unsigned long page_size = GETPAGESIZE();
const unsigned long std_page_size = 4096;
if(page_size <= std_page_size)
{
//use static executable memory we alocated via trampoline_dummy_func_for_mem
DATATYPE_ADDRESS addrs = (DATATYPE_ADDRESS)trampoline_dummy_func_for_mem + (page_size - 1);
trampoline_mem = (void*)(addrs & ~(page_size - 1));
sql_print_information(
"%s mem func addr: %p mem start addr: %p page size: %ld",
log_prefix, trampoline_dummy_func_for_mem, trampoline_mem, page_size);
}
else //big pages for some reason. allocate mem using mmap
{
trampoline_mem = mmap(NULL, page_size, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if(MAP_FAILED == trampoline_mem)
{
sql_print_error("%s unable to mmap memory size: %lu, errno: %d. Aborting.",
log_prefix, page_size, errno);
DBUG_RETURN(1);
}
else
{
sql_print_information(
"%s mem via mmap: %p page size: %ld", log_prefix, trampoline_mem, page_size);
}
}
trampoline_mem_free = trampoline_mem;
//hot patch stuff
void * target_function = NULL;
if(do_hot_patch((void **)&trampoline_mysql_execute_command, &trampoline_mysql_execute_size,
(void *)mysql_execute_command, (void *)audit_mysql_execute_command, "mysql_execute_command"))
{
DBUG_RETURN(1);
}
#if MYSQL_VERSION_ID < 50600
if(do_hot_patch((void **)&trampoline_log_slow_statement, &trampoline_log_slow_statement_size,
(void *)log_slow_statement, (void *)audit_log_slow_statement, "log_slow_statement"))
{
sql_print_error("%s Failed hot patch. Continuing as non-critical.",
log_prefix);
}
#endif
#if MYSQL_VERSION_ID < 50505
if(do_hot_patch((void **)&trampoline_check_user, &trampoline_check_user_size,
(void *)check_user, (void *)audit_check_user, "check_user"))
{
DBUG_RETURN(1);
}
#elif MYSQL_VERSION_ID < 50600
if(do_hot_patch((void **)&trampoline_acl_authenticate, &trampoline_acl_authenticate_size,
(void *)acl_authenticate, (void *)audit_acl_authenticate, "acl_authenticate"))
{
DBUG_RETURN(1);
}
#endif
int (Query_cache::*pf_send_result_to_client)(THD *,char *, uint) = &Query_cache::send_result_to_client;
target_function = *(void **) &pf_send_result_to_client;
if(do_hot_patch((void **)&trampoline_send_result_to_client, &trampoline_send_result_to_client_size,
(void *)target_function, (void *)audit_send_result_to_client, "send_result_to_client"))
{
DBUG_RETURN(1);
}
if(do_hot_patch((void **)&trampoline_check_table_access, &trampoline_check_table_access_size,
(void *)check_table_access, (void *)audit_check_table_access, "check_table_access"))
{
DBUG_RETURN(1);
}
#if MYSQL_VERSION_ID > 50505
target_function = (void *)*(bool (*)(THD *thd, TABLE_LIST **start, uint *counter, uint flags,
Prelocking_strategy *prelocking_strategy)) &open_tables;
if(do_hot_patch((void **)&trampoline_open_tables, &trampoline_open_tables_size,
(void *)target_function, (void *)audit_open_tables, "open_tables"))
{
DBUG_RETURN(1);
}
#else
target_function = (void *)*(int (*)(THD *thd, TABLE_LIST **start, uint *counter, uint flags)) &open_tables;
if(do_hot_patch((void **)&trampoline_open_tables, &trampoline_open_tables_size,
(void *)target_function, (void *)audit_open_tables, "open_tables"))
{
DBUG_RETURN(1);
}
#endif
if (set_com_status_vars_array () !=0)
{
DBUG_RETURN(1);
}
sql_print_information("%s Init completed successfully.", log_prefix);
DBUG_RETURN(0);
}
/*
plugin deinstallation.
SYNOPSIS
audit_plugin_deinit()
Does nothing.
RETURN VALUE
0 success
1 failure (cannot happen)
*/
static int audit_plugin_deinit(void *p)
{
DBUG_ENTER("audit_plugin_deinit");
sql_print_information("%s deinit", log_prefix);
remove_hot_functions();
DBUG_RETURN(0);
}
/*
Plugin status variables for SHOW STATUS
*/
static struct st_mysql_show_var audit_status[] =
{
{ "Audit_version",
(char *) MYSQL_AUDIT_PLUGIN_VERSION "-" MYSQL_AUDIT_PLUGIN_REVISION,
SHOW_CHAR },
{ "Audit_protocol_version",
(char *) AUDIT_PROTOCOL_VERSION,
SHOW_CHAR },
//{"called", (char *)&number_of_calls, SHOW_LONG},
{ 0, 0, (enum_mysql_show_type) 0 } };
static void json_log_file_enable(THD *thd, struct st_mysql_sys_var *var,
void *tgt, const void *save)
{
json_file_handler_enable = *(my_bool *) save ? TRUE : FALSE;
if(json_file_handler.is_init())
{
json_file_handler.set_enable(json_file_handler_enable);
}
}
static void json_log_file_flush(THD *thd, struct st_mysql_sys_var *var,
void *tgt, const void *save)
{
//always set to false. as we just flush if set to true and leave at 0
json_file_handler_flush = FALSE;
my_bool val = *(my_bool *) save ? TRUE : FALSE;
if(val && json_file_handler.is_init())
{
json_file_handler.flush();
}
}
static void json_log_socket_enable(THD *thd, struct st_mysql_sys_var *var,
void *tgt, const void *save)
{
json_socket_handler_enable = *(my_bool *) save ? TRUE : FALSE;
if(json_socket_handler.is_init())
{
json_socket_handler.set_enable(json_socket_handler_enable);
}
}
//setup sysvars which update directly the relevant plugins
static MYSQL_SYSVAR_BOOL(header_msg, json_formatter.m_write_start_msg,
PLUGIN_VAR_RQCMDARG,
"AUDIT write header message at start of logging or file flush Enable|Disable. Default enabled.", NULL, NULL, 1);
static MYSQL_SYSVAR_BOOL(force_record_logins, force_record_logins_enable,
PLUGIN_VAR_RQCMDARG,
"AUDIT force record Connect, Quit and Failed Login commands, regardless of the settings in audit_record_cmds and audit_record_objs Enable|Disable. Default disabled.", NULL, NULL, 0);
static MYSQL_SYSVAR_STR(json_log_file, json_file_handler.m_io_dest,
PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_MEMALLOC,
"AUDIT plugin json log file name",
NULL, NULL, "mysql-audit.json");
static MYSQL_SYSVAR_LONG(json_file_bufsize, json_file_handler.m_bufsize,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin json log file buffer size. Buffer size in bytes (lager size may improve performance). 0 = use default size, 1 = no buffering. If changed during runtime need to perform a flush for the new value to take affect.",
NULL, NULL, 0, 1, 262144, 0);
static MYSQL_SYSVAR_UINT(json_file_sync, json_file_handler.m_sync_period,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin json log file sync period. If the value of this variable is greater than 0, audit log will sync to disk after every audit_json_file_sync writes.",
NULL, NULL, 0, 0, UINT_MAX32, 0);
static MYSQL_SYSVAR_UINT(json_file_retry, json_file_handler.m_retry_interval,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin json log file retry interval. If the plugin fails to open/write to the json log file, will retry to open every specified interval in seconds. Set for 0 to disable retrying. Default 60 seconds.",
NULL, NULL, 60, 0, UINT_MAX32, 0);
static MYSQL_SYSVAR_UINT(json_socket_retry, json_socket_handler.m_retry_interval,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin json socket connect interval. If the plugin fails to connect/write to the json audit socket, will retry to connect every specified interval in seconds. Set for 0 to disable retrying. Default 10 seconds.",
NULL, NULL, 10, 0, UINT_MAX32, 0);
static MYSQL_SYSVAR_BOOL(json_file, json_file_handler_enable,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin json log file Enable|Disable", NULL, json_log_file_enable, 0);
static MYSQL_SYSVAR_BOOL(json_file_flush, json_file_handler_flush,
PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_NOCMDOPT,
"AUDIT plugin json log file flush. Set to ON to perform a flush of the log.", NULL, json_log_file_flush, 0);
static MYSQL_SYSVAR_STR(json_socket_name, json_socket_handler.m_io_dest,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin json log unix socket name",
NULL, json_socket_name_update, "");
static MYSQL_SYSVAR_STR(offsets, offsets_string,
PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY | PLUGIN_VAR_MEMALLOC,
"AUDIT plugin offsets. Comma separated list of offsets to use for extracting data",
NULL, NULL, NULL);
static MYSQL_SYSVAR_STR(checksum, checksum_string,
PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY | PLUGIN_VAR_MEMALLOC,
"AUDIT plugin checksum. Checksum for mysqld corresponding to offsets",
NULL, NULL, "");
#define _COMMENT_SPACE_ "(?:/\\*.*?\\*/|\\s)*?"
#define _QUOTED_PSW_ "[\'|\"](?<psw>.*?)(?<!\\\\)[\'|\"]"
static MYSQL_SYSVAR_STR(password_masking_regex, password_masking_regex_string,
PLUGIN_VAR_RQCMDARG ,
"AUDIT plugin regex to use for password masking",
password_masking_regex_check, password_masking_regex_string_update,
//identified by [password] '***'
"identified"_COMMENT_SPACE_"by"_COMMENT_SPACE_"(?:password)?"_COMMENT_SPACE_ _QUOTED_PSW_
//password function
"|password"_COMMENT_SPACE_"\\("_COMMENT_SPACE_ _QUOTED_PSW_ _COMMENT_SPACE_"\\)"
//Used at: CHANGE MASTER TO MASTER_PASSWORD='new3cret', SET PASSWORD [FOR user] = 'hash', password 'user_pass';
"|password"_COMMENT_SPACE_"(?:for"_COMMENT_SPACE_"\\S+?)?"_COMMENT_SPACE_"="_COMMENT_SPACE_ _QUOTED_PSW_
"|password"_COMMENT_SPACE_ _QUOTED_PSW_
//federated engine create table with connection. See: http://dev.mysql.com/doc/refman/5.5/en/federated-create-connection.html
//commented out as federated engine is disabled by default
//"|ENGINE"_COMMENT_SPACE_"="_COMMENT_SPACE_"FEDERATED"_COMMENT_SPACE_".*CONNECTION"_COMMENT_SPACE_"="_COMMENT_SPACE_"[\'|\"]\\S+?://\\S+?:(?<psw>.*)@\\S+[\'|\"]"
);
static MYSQL_SYSVAR_BOOL(uninstall_plugin, uninstall_plugin_enable,
PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY ,
"AUDIT uninstall plugin Enable|Disable. Default disabled. If disabled attempts to uninstall the AUDIT plugin via the sql UNINSTALL command will fail.", NULL, NULL, 0);
static MYSQL_SYSVAR_BOOL(offsets_by_version, offsets_by_version_enable,
PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY ,
"AUDIT plugin search offsets by version. If checksum validation doesn't pass will attempt to load and validate offsets according to version. Enable|Disable", NULL, NULL, 1);
static MYSQL_SYSVAR_BOOL(validate_checksum, validate_checksum_enable,
PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY ,
"AUDIT plugin binary checksum validation Enable|Disable", NULL, NULL, 1);
static MYSQL_SYSVAR_BOOL(validate_offsets_extended, validate_offsets_extended_enable,
PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY ,
"AUDIT plugin offset extended validation Enable|Disable", NULL, NULL, 1);
static MYSQL_SYSVAR_BOOL(json_socket, json_socket_handler_enable,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin json log unix socket Enable|Disable", NULL, json_log_socket_enable, 0);
static MYSQL_SYSVAR_INT(delay_ms, delay_ms_val,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin delay in miliseconds. Delay amount injection. If 0 or negative then delay is disabled.",
NULL, NULL, 0, 0, INT_MAX32, 0);
static MYSQL_SYSVAR_STR(delay_cmds, delay_cmds_string,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin delay commands to match against comma separated. If empty then delay is disabled.",
NULL, delay_cmds_string_update, NULL);
static MYSQL_SYSVAR_STR(whitelist_cmds, whitelist_cmds_string,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin commands to skip, comma separated",
NULL, whitelist_cmds_string_update, NULL);
static MYSQL_SYSVAR_STR(record_cmds, record_cmds_string,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin whitelisted commands for which queries are not recorded, comma separated",
NULL, record_cmds_string_update, NULL);
static MYSQL_SYSVAR_STR(password_masking_cmds, password_masking_cmds_string,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin commands to apply password masking regex to, comma separated",
NULL, password_masking_cmds_string_update,
//set passowrd is recoreded as set_option
"CREATE_USER,GRANT,SET_OPTION,SLAVE_START,CREATE_SERVER,ALTER_SERVER,CHANGE_MASTER");
static MYSQL_SYSVAR_STR(whitelist_users, whitelist_users_string,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin whitelisted users whose queries are not recorded, comma separated",
NULL, whitelist_users_string_update, NULL);
static MYSQL_SYSVAR_STR(record_objs, record_objs_string,
PLUGIN_VAR_RQCMDARG,
"AUDIT plugin objects to record, comma separated",
NULL, record_objs_string_update_extended, NULL);
/*
* Plugin system vars
*/
static struct st_mysql_sys_var* audit_system_variables[] =
{
MYSQL_SYSVAR(header_msg),
MYSQL_SYSVAR(force_record_logins),
MYSQL_SYSVAR(json_log_file),
MYSQL_SYSVAR(json_file_bufsize),
MYSQL_SYSVAR(json_file_sync),
MYSQL_SYSVAR(json_file_retry),
MYSQL_SYSVAR(json_socket_retry),
MYSQL_SYSVAR(json_file),
MYSQL_SYSVAR(json_file_flush),
MYSQL_SYSVAR(uninstall_plugin),
MYSQL_SYSVAR(validate_checksum),
MYSQL_SYSVAR(offsets_by_version),
MYSQL_SYSVAR(validate_offsets_extended),
MYSQL_SYSVAR(json_socket_name),
MYSQL_SYSVAR(offsets),
MYSQL_SYSVAR(json_socket),
MYSQL_SYSVAR(query_cache_table_list),
MYSQL_SYSVAR(is_thd_printed_list),
MYSQL_SYSVAR(delay_ms),
MYSQL_SYSVAR(delay_cmds),
MYSQL_SYSVAR(whitelist_cmds),
MYSQL_SYSVAR(record_cmds),
MYSQL_SYSVAR(password_masking_cmds),
MYSQL_SYSVAR(whitelist_users),
MYSQL_SYSVAR(record_objs),
MYSQL_SYSVAR(checksum),
MYSQL_SYSVAR(password_masking_regex),
NULL
};
//declare our plugin
mysql_declare_plugin(audit_plugin)
{
plugin_type,
&audit_plugin,
"AUDIT",
"McAfee Inc",
"AUDIT plugin, creates a file mysql-audit.log to log activity",
PLUGIN_LICENSE_GPL,
audit_plugin_init, /* Plugin Init */
audit_plugin_deinit, /* Plugin Deinit */
0x0100 /* 1.0 */,
audit_status, /* status variables */
audit_system_variables, /* system variables */
NULL /* config options */
}
mysql_declare_plugin_end;
#if MYSQL_VERSION_ID < 50505
/**
* DLL constructor method.
* We set here the audit plugin version to the same as the first built in plugin.
* This is so we can have a single lib for all versions (needed in 5.1)
*/
extern "C" void __attribute__ ((constructor)) audit_plugin_so_init(void)
{
if (mysqld_builtins && mysqld_builtins[0])
{
audit_plugin.interface_version = *(int *) mysqld_builtins[0]->info;
sql_print_information("%s Set interface version to: %d (%d)",
log_prefix, audit_plugin.interface_version,
audit_plugin.interface_version >> 8);
}
else
{
sql_print_error(
"%s mysqld_builtins are null. Plugin will not load unless the mysql version is: %d. \n",
log_prefix, audit_plugin.interface_version >> 8);
}
}
#elif MYSQL_VERSION_ID < 50600
extern struct st_mysql_plugin *mysql_mandatory_plugins[];
extern "C" void __attribute__ ((constructor)) audit_plugin_so_init(void)
{
audit_plugin.interface_version = *(int *) mysql_mandatory_plugins[0]->info;
sql_print_information("%s Set interface version to: %d (%d)",
log_prefix, audit_plugin.interface_version,
audit_plugin.interface_version >> 8);
}
#elif !defined(MARIADB_BASE_VERSION)
//interface version for MySQL 5.6 changed in 5.6.14
extern "C" void __attribute__ ((constructor)) audit_plugin_so_init(void)
{
const char * ver_5_6_13 = "5.6.13";
if(strncmp(server_version, ver_5_6_13, strlen(ver_5_6_13)) <= 0)
{
audit_plugin.interface_version = 0x0300;
}
else
{
audit_plugin.interface_version = 0x0301;
}
}
#endif
/*
Pure virtual handler. Needed when running in mysql compiled with a newer version of gcc.
Versions of mysql for RH 6 and Percona this function is defined local in mysqld.
So we define our own implementation.
*/
extern "C" int __cxa_pure_virtual (void)
{
sql_print_error(
"%s __cxa_pure_virtual called. Fatal condition. ",
log_prefix);
return 0;
}
/*
* Variable to hold version
*/
MYSQL_AUDIT_PLUGIN_SYMBOL_VERSION() = '\0';