Files
sudo/plugins/audit_json/audit_json.c
Todd C. Miller f0dc48548c Add support for "accept" audit events sent by the sudo front-end.
With this change, the sudo front-end will send an "accept" audit
event to the audit plugins after all the I/O logging plugins have
been initialized.  This can be used by an audit plugin that does
not care about the result of the individual policy and approval
plugins and only wants to receive a single "accept" event if all
policy and approval plugins have succeeded.  The plugin_type argument
for events sent by the front-end is SUDO_FRONT_END (0).
2020-06-02 09:07:44 -06:00

699 lines
18 KiB
C

/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2020 Todd C. Miller <Todd.Miller@sudo.ws>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* This is an open source non-commercial project. Dear PVS-Studio, please check it.
* PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
*/
#include <config.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include "compat/stdbool.h"
#endif /* HAVE_STDBOOL_H */
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include <time.h>
#include "sudo_gettext.h" /* must be included before sudo_compat.h */
#include "sudo_compat.h"
#include "sudo_conf.h"
#include "sudo_debug.h"
#include "sudo_dso.h"
#include "sudo_fatal.h"
#include "sudo_json.h"
#include "sudo_plugin.h"
#include "sudo_util.h"
#include "pathnames.h"
static int audit_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER;
static sudo_conv_t audit_conv;
static sudo_printf_t audit_printf;
static struct audit_state {
int submit_optind;
char uuid_str[37];
bool accepted;
FILE *log_fp;
char *logfile;
char * const * settings;
char * const * user_info;
char * const * submit_argv;
char * const * submit_envp;
} state = { -1 };
/* Filter out entries in settings[] that are not really options. */
char * const settings_filter[] = {
"debug_flags",
"max_groups",
"network_addrs",
"plugin_dir",
"plugin_path",
"progname",
NULL
};
static int
audit_json_open(unsigned int version, sudo_conv_t conversation,
sudo_printf_t plugin_printf, char * const settings[],
char * const user_info[], int submit_optind, char * const submit_argv[],
char * const submit_envp[], char * const plugin_options[],
const char **errstr)
{
struct sudo_conf_debug_file_list debug_files =
TAILQ_HEAD_INITIALIZER(debug_files);
struct sudo_debug_file *debug_file;
const char *cp, *plugin_path = NULL;
unsigned char uuid[16];
char * const *cur;
mode_t oldmask;
int fd, ret = -1;
debug_decl_vars(audit_json_open, SUDO_DEBUG_PLUGIN);
audit_conv = conversation;
audit_printf = plugin_printf;
/*
* Stash initial values.
*/
state.submit_optind = submit_optind;
state.settings = settings;
state.user_info = user_info;
state.submit_argv = submit_argv;
state.submit_envp = submit_envp;
/* Initialize the debug subsystem. */
for (cur = settings; (cp = *cur) != NULL; cur++) {
if (strncmp(cp, "debug_flags=", sizeof("debug_flags=") - 1) == 0) {
cp += sizeof("debug_flags=") - 1;
if (sudo_debug_parse_flags(&debug_files, cp) == -1)
goto oom;
continue;
}
if (strncmp(cp, "plugin_path=", sizeof("plugin_path=") - 1) == 0) {
plugin_path = cp + sizeof("plugin_path=") - 1;
continue;
}
}
if (plugin_path != NULL && !TAILQ_EMPTY(&debug_files)) {
audit_debug_instance =
sudo_debug_register(plugin_path, NULL, NULL, &debug_files);
if (audit_debug_instance == SUDO_DEBUG_INSTANCE_ERROR) {
*errstr = U_("unable to initialize debugging");
goto bad;
}
sudo_debug_enter(__func__, __FILE__, __LINE__, sudo_debug_subsys);
}
/* Create a UUID for this command for use with audit records. */
sudo_uuid_create(uuid);
if (sudo_uuid_to_string(uuid, state.uuid_str, sizeof(state.uuid_str)) == NULL) {
*errstr = U_("unable to generate UUID");
goto bad;
}
/* Parse plugin_options to check for logfile option. */
if (plugin_options != NULL) {
for (cur = plugin_options; (cp = *cur) != NULL; cur++) {
if (strncmp(cp, "logfile=", sizeof("logfile=") - 1) == 0) {
state.logfile = strdup(cp + sizeof("logfile=") - 1);
if (state.logfile == NULL)
goto oom;
}
}
}
if (state.logfile == NULL) {
if (asprintf(&state.logfile, "%s/sudo_audit.json", _PATH_SUDO_LOGDIR) == -1)
goto oom;
}
/* open log file */
/* TODO: support pipe */
oldmask = umask(S_IRWXG|S_IRWXO);
fd = open(state.logfile, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
(void)umask(oldmask);
if (fd == -1 || (state.log_fp = fdopen(fd, "w")) == NULL) {
*errstr = U_("unable to open audit system");
if (fd != -1)
close(fd);
goto bad;
}
ret = 1;
goto done;
oom:
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
*errstr = U_("unable to allocate memory");
bad:
if (state.log_fp != NULL) {
fclose(state.log_fp);
state.log_fp = NULL;
}
done:
while ((debug_file = TAILQ_FIRST(&debug_files))) {
TAILQ_REMOVE(&debug_files, debug_file, entries);
free(debug_file->debug_file);
free(debug_file->debug_flags);
free(debug_file);
}
debug_return_int(ret);
}
static bool
add_key_value(struct json_container *json, const char *str)
{
struct json_value json_value;
const char *cp, *errstr;
char name[256];
size_t len;
debug_decl(add_key_value, SUDO_DEBUG_PLUGIN);
if ((cp = strchr(str, '=')) == NULL) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"ignoring bad command info string \"%s\"", str);
debug_return_bool(false);
}
len = (size_t)(cp - str);
cp++;
/* Variable name currently limited to 256 chars */
if (len >= sizeof(name)) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"ignoring long command info name \"%.*s\"", (int)len, str);
debug_return_bool(false);
}
memcpy(name, str, len);
name[len] = '\0';
/* Check for bool or number. */
json_value.type = JSON_NULL;
switch (*cp) {
case '+': case '-': case '0': case '1': case '2': case '3':
case '4': case '5': case '6': case '7': case '8': case '9':
json_value.u.number = sudo_strtonum(cp, INT_MIN, INT_MAX, &errstr);
if (errstr == NULL)
json_value.type = JSON_NUMBER;
break;
case 't':
if (strcmp(cp, "true") == 0) {
json_value.type = JSON_BOOL;
json_value.u.boolean = true;
}
break;
case 'f':
if (strcmp(cp, "false") == 0) {
json_value.type = JSON_BOOL;
json_value.u.boolean = false;
}
break;
}
/* Default to string type. */
if (json_value.type == JSON_NULL) {
json_value.type = JSON_STRING;
json_value.u.string = cp;
}
debug_return_bool(sudo_json_add_value(json, name, &json_value));
}
static bool
add_array(struct json_container *json, const char *name, char * const * array)
{
const char *cp;
struct json_value json_value;
debug_decl(add_array, SUDO_DEBUG_PLUGIN);
if (!sudo_json_open_array(json, name))
debug_return_bool(false);
while ((cp = *array) != NULL) {
json_value.type = JSON_STRING;
json_value.u.string = cp;
if (!sudo_json_add_value(json, name, &json_value))
debug_return_bool(false);
array++;
}
if (!sudo_json_close_array(json))
debug_return_bool(false);
debug_return_bool(true);
}
static bool
filter_key_value(const char *kv, char * const * filter)
{
char * const *cur;
const char *cp;
size_t namelen;
if (filter != NULL) {
namelen = strcspn(kv, "=");
for (cur = filter; (cp = *cur) != NULL; cur++) {
if (strncmp(kv, cp, namelen) == 0 && cp[namelen] == '\0')
return true;
}
}
return false;
}
static bool
add_key_value_object(struct json_container *json, const char *name,
char * const * array, char * const * filter)
{
char * const *cur;
const char *cp;
bool empty = false;
debug_decl(add_key_value_object, SUDO_DEBUG_PLUGIN);
if (filter != NULL) {
/* Avoid printing an empty object if everything is filtered. */
empty = true;
for (cur = array; (cp = *cur) != NULL; cur++) {
if (!filter_key_value(cp, filter)) {
empty = false;
break;
}
}
}
if (!empty) {
if (!sudo_json_open_object(json, name))
goto bad;
for (cur = array; (cp = *cur) != NULL; cur++) {
if (filter_key_value(cp, filter))
continue;
if (!add_key_value(json, cp))
goto bad;
}
if (!sudo_json_close_object(json))
goto bad;
}
debug_return_bool(true);
bad:
debug_return_bool(false);
}
static bool
add_timestamp(struct json_container *json, struct timespec *ts)
{
struct json_value json_value;
time_t secs = ts->tv_sec;
char timebuf[1024];
struct tm *tm;
debug_decl(add_timestamp, SUDO_DEBUG_PLUGIN);
if ((tm = gmtime(&secs)) == NULL)
debug_return_bool(false);
sudo_json_open_object(json, "timestamp");
json_value.type = JSON_NUMBER;
json_value.u.number = ts->tv_sec;
sudo_json_add_value(json, "seconds", &json_value);
json_value.type = JSON_NUMBER;
json_value.u.number = ts->tv_nsec;
sudo_json_add_value(json, "nanoseconds", &json_value);
strftime(timebuf, sizeof(timebuf), "%Y%m%d%H%M%SZ", tm);
json_value.type = JSON_STRING;
json_value.u.string = timebuf;
sudo_json_add_value(json, "iso8601", &json_value);
strftime(timebuf, sizeof(timebuf), "%a %b %e %H:%M:%S %Z %Y", tm);
json_value.type = JSON_STRING;
json_value.u.string = timebuf;
sudo_json_add_value(json, "localtime", &json_value);
sudo_json_close_object(json);
debug_return_bool(true);
}
static int
audit_write_json(struct json_container *json)
{
struct stat sb;
int ret = -1;
debug_decl(audit_write_json, SUDO_DEBUG_PLUGIN);
if (!sudo_lock_file(fileno(state.log_fp), SUDO_LOCK)) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
"unable to lock %s", state.logfile);
goto done;
}
/* Note: assumes file ends in "\n}\n" */
if (fstat(fileno(state.log_fp), &sb) == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
"unable to stat %s", state.logfile);
goto done;
}
if (sb.st_size == 0) {
/* New file */
putc('{', state.log_fp);
} else if (fseeko(state.log_fp, -3, SEEK_END) == 0) {
/* Continue file, overwrite the final "\n}\n" */
putc(',', state.log_fp);
} else {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
"unable to seek %s", state.logfile);
goto done;
}
fputs(sudo_json_get_buf(json), state.log_fp);
fputs("\n}\n", state.log_fp);
fflush(state.log_fp);
(void)sudo_lock_file(fileno(state.log_fp), SUDO_UNLOCK);
/* TODO: undo partial record on error */
if (!ferror(state.log_fp))
ret = true;
done:
debug_return_int(ret);
}
static int
audit_write_exit_record(int exit_status, int error)
{
struct json_container json;
struct json_value json_value;
struct timespec now;
int ret = -1;
debug_decl(audit_write_exit_record, SUDO_DEBUG_PLUGIN);
if (sudo_gettime_real(&now) == -1) {
sudo_warn(U_("unable to read the clock"));
goto done;
}
if (!sudo_json_init(&json, 4, false, false))
goto oom;
if (!sudo_json_open_object(&json, "exit"))
goto oom;
/* Write UUID */
json_value.type = JSON_STRING;
json_value.u.string = state.uuid_str;
if (!sudo_json_add_value(&json, "uuid", &json_value))
goto oom;
/* Write time stamp */
if (!add_timestamp(&json, &now))
goto oom;
if (error != 0) {
/* Error executing command */
json_value.type = JSON_STRING;
json_value.u.string = strerror(error);
if (!sudo_json_add_value(&json, "error", &json_value))
goto oom;
} else {
if (WIFEXITED(exit_status)) {
/* Command exited normally. */
json_value.type = JSON_NUMBER;
json_value.u.number = WEXITSTATUS(exit_status);
if (!sudo_json_add_value(&json, "exit_value", &json_value))
goto oom;
} else if (WIFSIGNALED(exit_status)) {
/* Command killed by signal. */
char signame[SIG2STR_MAX];
int signo = WTERMSIG(exit_status);
if (signo <= 0 || sig2str(signo, signame) == -1) {
json_value.type = JSON_NUMBER;
json_value.u.number = signo;
if (!sudo_json_add_value(&json, "signal", &json_value))
goto oom;
} else {
json_value.type = JSON_STRING;
json_value.u.string = signame;
if (!sudo_json_add_value(&json, "signal", &json_value))
goto oom;
}
/* Core dump? */
json_value.type = JSON_BOOL;
json_value.u.boolean = WCOREDUMP(exit_status);
if (!sudo_json_add_value(&json, "dumped_core", &json_value))
goto oom;
/* Exit value */
json_value.type = JSON_NUMBER;
json_value.u.number = WTERMSIG(exit_status) | 128;
if (!sudo_json_add_value(&json, "exit_value", &json_value))
goto oom;
}
}
if (!sudo_json_close_object(&json))
goto oom;
ret = audit_write_json(&json);
sudo_json_free(&json);
done:
debug_return_int(ret);
oom:
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
sudo_json_free(&json);
debug_return_int(-1);
}
static int
audit_write_record(const char *audit_str, const char *plugin_name,
unsigned int plugin_type, const char *reason, char * const command_info[],
char * const run_argv[], char * const run_envp[])
{
struct json_container json;
struct json_value json_value;
struct timespec now;
int ret = -1;
debug_decl(audit_write_record, SUDO_DEBUG_PLUGIN);
if (sudo_gettime_real(&now) == -1) {
sudo_warn(U_("unable to read the clock"));
goto done;
}
if (!sudo_json_init(&json, 4, false, false))
goto oom;
if (!sudo_json_open_object(&json, audit_str))
goto oom;
json_value.type = JSON_STRING;
json_value.u.string = plugin_name;
if (!sudo_json_add_value(&json, "plugin_name", &json_value))
goto oom;
switch (plugin_type) {
case SUDO_FRONT_END:
json_value.u.string = "front-end";
break;
case SUDO_POLICY_PLUGIN:
json_value.u.string = "policy";
break;
case SUDO_IO_PLUGIN:
json_value.u.string = "io";
break;
case SUDO_APPROVAL_PLUGIN:
json_value.u.string = "approval";
break;
case SUDO_AUDIT_PLUGIN:
json_value.u.string = "audit";
break;
default:
json_value.u.string = "unknown";
break;
}
json_value.type = JSON_STRING;
if (!sudo_json_add_value(&json, "plugin_type", &json_value))
goto oom;
/* error and reject audit events usually contain a reason. */
if (reason != NULL) {
json_value.type = JSON_STRING;
json_value.u.string = reason;
if (!sudo_json_add_value(&json, "reason", &json_value))
goto oom;
}
json_value.type = JSON_STRING;
json_value.u.string = state.uuid_str;
if (!sudo_json_add_value(&json, "uuid", &json_value))
goto oom;
if (!add_timestamp(&json, &now))
goto oom;
/* Write key=value objects. */
if (!add_key_value_object(&json, "options", state.settings, settings_filter))
goto oom;
if (!add_key_value_object(&json, "user_info", state.user_info, NULL))
goto oom;
if (command_info != NULL) {
if (!add_key_value_object(&json, "command_info", command_info, NULL))
goto oom;
}
/* Write submit_optind before submit_argv */
json_value.type = JSON_NUMBER;
json_value.u.number = state.submit_optind;
if (!sudo_json_add_value(&json, "submit_optind", &json_value))
goto oom;
if (!add_array(&json, "submit_argv", state.submit_argv))
goto oom;
if (!add_array(&json, "submit_envp", state.submit_envp))
goto oom;
if (run_argv != NULL) {
if (!add_array(&json, "run_argv", run_argv))
goto oom;
}
if (run_envp != NULL) {
if (!add_array(&json, "run_envp", run_envp))
goto oom;
}
if (!sudo_json_close_object(&json))
goto oom;
ret = audit_write_json(&json);
sudo_json_free(&json);
done:
debug_return_int(ret);
oom:
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
sudo_json_free(&json);
debug_return_int(-1);
}
static int
audit_json_accept(const char *plugin_name, unsigned int plugin_type,
char * const command_info[], char * const run_argv[],
char * const run_envp[], const char **errstr)
{
int ret;
debug_decl(audit_json_accept, SUDO_DEBUG_PLUGIN);
/* Ignore the extra accept event from the sudo front-end. */
if (plugin_type == SUDO_FRONT_END)
debug_return_int(true);
state.accepted = true;
ret = audit_write_record("accept", plugin_name, plugin_type, NULL,
command_info, run_argv, run_envp);
debug_return_int(ret);
}
static int
audit_json_reject(const char *plugin_name, unsigned int plugin_type,
const char *reason, char * const command_info[], const char **errstr)
{
int ret;
debug_decl(audit_json_reject, SUDO_DEBUG_PLUGIN);
ret = audit_write_record("reject", plugin_name, plugin_type,
reason, command_info, NULL, NULL);
debug_return_int(ret);
}
static int
audit_json_error(const char *plugin_name, unsigned int plugin_type,
const char *reason, char * const command_info[], const char **errstr)
{
int ret;
debug_decl(audit_json_error, SUDO_DEBUG_PLUGIN);
ret = audit_write_record("error", plugin_name, plugin_type,
reason, command_info, NULL, NULL);
debug_return_int(ret);
}
static void
audit_json_close(int status_type, int status)
{
debug_decl(audit_json_close, SUDO_DEBUG_PLUGIN);
switch (status_type) {
case SUDO_PLUGIN_NO_STATUS:
break;
case SUDO_PLUGIN_WAIT_STATUS:
audit_write_exit_record(status, 0);
break;
case SUDO_PLUGIN_EXEC_ERROR:
audit_write_exit_record(0, status);
break;
case SUDO_PLUGIN_SUDO_ERROR:
audit_write_record("error", "sudo", 0, strerror(status),
NULL, NULL, NULL);
break;
default:
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"unexpected status type %d, value %d", status_type, status);
break;
}
free(state.logfile);
if (state.log_fp != NULL)
fclose(state.log_fp);
debug_return;
}
static int
audit_json_show_version(int verbose)
{
debug_decl(audit_json_show_version, SUDO_DEBUG_PLUGIN);
audit_printf(SUDO_CONV_INFO_MSG, "JSON audit plugin version %s\n",
PACKAGE_VERSION);
debug_return_int(true);
}
__dso_public struct audit_plugin audit_json = {
SUDO_AUDIT_PLUGIN,
SUDO_API_VERSION,
audit_json_open,
audit_json_close,
audit_json_accept,
audit_json_reject,
audit_json_error,
audit_json_show_version,
NULL, /* register_hooks */
NULL /* deregister_hooks */
};