summaryrefslogtreecommitdiffstats
path: root/plugins/audit_json
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 13:14:46 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 13:14:46 +0000
commit025c439e829e0db9ac511cd9c1b8d5fd53475ead (patch)
treefa6986b4690f991613ffb97cea1f6942427baf5d /plugins/audit_json
parentInitial commit. (diff)
downloadsudo-upstream/1.9.15p5.tar.xz
sudo-upstream/1.9.15p5.zip
Adding upstream version 1.9.15p5.upstream/1.9.15p5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins/audit_json')
-rw-r--r--plugins/audit_json/Makefile.in232
-rw-r--r--plugins/audit_json/audit_json.c737
-rw-r--r--plugins/audit_json/audit_json.exp1
3 files changed, 970 insertions, 0 deletions
diff --git a/plugins/audit_json/Makefile.in b/plugins/audit_json/Makefile.in
new file mode 100644
index 0000000..e885a39
--- /dev/null
+++ b/plugins/audit_json/Makefile.in
@@ -0,0 +1,232 @@
+#
+# SPDX-License-Identifier: ISC
+#
+# Copyright (c) 2020-2023 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.
+#
+# @configure_input@
+#
+
+#### Start of system configuration section. ####
+
+srcdir = @srcdir@
+devdir = @devdir@
+top_builddir = @top_builddir@
+abs_top_builddir = @abs_top_builddir@
+top_srcdir = @top_srcdir@
+scriptdir = $(top_srcdir)/scripts
+incdir = $(top_srcdir)/include
+cross_compiling = @CROSS_COMPILING@
+
+# Compiler & tools to use
+CC = @CC@
+LIBTOOL = @LIBTOOL@
+SED = @SED@
+AWK = @AWK@
+
+# Our install program supports extra flags...
+INSTALL = $(SHELL) $(scriptdir)/install-sh -c
+INSTALL_OWNER = -o $(install_uid) -g $(install_gid)
+INSTALL_BACKUP = @INSTALL_BACKUP@
+
+# Libraries
+LT_LIBS = $(top_builddir)/lib/util/libsudo_util.la
+LIBS = $(LT_LIBS)
+
+# C preprocessor flags
+CPPFLAGS = -I$(incdir) -I$(top_builddir) @CPPFLAGS@
+
+# Usually -O and/or -g
+CFLAGS = @CFLAGS@
+
+# Flags to pass to the link stage
+LDFLAGS = @LDFLAGS@
+LT_LDFLAGS = @LT_LDFLAGS@ @LT_LDEXPORTS@
+
+# Flags to pass to libtool
+LTFLAGS = --tag=disable-static
+
+# Address sanitizer flags
+ASAN_CFLAGS = @ASAN_CFLAGS@
+ASAN_LDFLAGS = @ASAN_LDFLAGS@
+
+# PIE flags
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+
+# Stack smashing protection flags
+HARDENING_CFLAGS = @HARDENING_CFLAGS@
+HARDENING_LDFLAGS = @HARDENING_LDFLAGS@
+
+# cppcheck options, usually set in the top-level Makefile
+CPPCHECK_OPTS = -q --enable=warning,performance,portability --suppress=constStatement --suppress=compareBoolExpressionWithInt --error-exitcode=1 --inline-suppr -Dva_copy=va_copy -U__cplusplus -UQUAD_MAX -UQUAD_MIN -UUQUAD_MAX -U_POSIX_HOST_NAME_MAX -U_POSIX_PATH_MAX -U__NBBY -DNSIG=64
+
+# splint options, usually set in the top-level Makefile
+SPLINT_OPTS = -D__restrict= -checks
+
+# PVS-studio options
+PVS_CFG = $(top_srcdir)/PVS-Studio.cfg
+PVS_IGNORE = 'V707,V011,V002,V536'
+PVS_LOG_OPTS = -a 'GA:1,2' -e -t errorfile -d $(PVS_IGNORE)
+
+# Where to install things...
+prefix = @prefix@
+exec_prefix = @exec_prefix@
+bindir = @bindir@
+sbindir = @sbindir@
+sysconfdir = @sysconfdir@
+adminconfdir = @adminconfdir@
+libexecdir = @libexecdir@
+datarootdir = @datarootdir@
+localstatedir = @localstatedir@
+plugindir = @plugindir@
+
+# File mode and map file to use for shared libraries/objects
+shlib_enable = @SHLIB_ENABLE@
+shlib_mode = @SHLIB_MODE@
+shlib_exp = $(srcdir)/audit_json.exp
+shlib_map = audit_json.map
+shlib_opt = audit_json.opt
+
+# User and group ids the installed files should be "owned" by
+install_uid = 0
+install_gid = 0
+
+#### End of system configuration section. ####
+
+SHELL = @SHELL@
+
+OBJS = audit_json.lo
+
+IOBJS = $(OBJS:.lo=.i)
+
+POBJS = $(IOBJS:.i=.plog)
+
+LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/
+
+VERSION = @PACKAGE_VERSION@
+
+all: audit_json.la
+
+depend:
+ $(scriptdir)/mkdep.pl --srcdir=$(top_srcdir) \
+ --builddir=$(abs_top_builddir) plugins/audit_json/Makefile.in
+ cd $(top_builddir) && ./config.status --file plugins/audit_json/Makefile
+
+Makefile: $(srcdir)/Makefile.in
+ cd $(top_builddir) && ./config.status --file plugins/audit_json/Makefile
+
+.SUFFIXES: .c .h .i .lo .plog
+
+.c.lo:
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $<
+
+.c.i:
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+
+.i.plog:
+ ifile=$<; rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $${ifile%i}c --i-file $< --output-file $@
+
+$(shlib_map): $(shlib_exp)
+ @$(AWK) 'BEGIN { print "{\n\tglobal:" } { print "\t\t"$$0";" } END { print "\tlocal:\n\t\t*;\n};" }' $(shlib_exp) > $@
+
+$(shlib_opt): $(shlib_exp)
+ @$(SED) 's/^/+e /' $(shlib_exp) > $@
+
+audit_json.la: $(OBJS) $(LT_LIBS) @LT_LDDEP@
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) $(LDFLAGS) $(ASAN_LDFLAGS) $(HARDENING_LDFLAGS) $(LT_LDFLAGS) -o $@ $(OBJS) $(LIBS) -module -avoid-version -rpath $(plugindir) -shrext .so
+
+pre-install:
+
+install: install-plugin
+
+install-dirs:
+ $(SHELL) $(scriptdir)/mkinstalldirs $(DESTDIR)$(plugindir)
+
+install-binaries:
+
+install-includes:
+
+install-doc:
+
+install-plugin: install-dirs audit_json.la
+ if [ X"$(shlib_enable)" = X"yes" ]; then \
+ INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m $(shlib_mode) audit_json.la $(DESTDIR)$(plugindir); \
+ fi
+
+install-fuzzer:
+
+uninstall:
+ -$(LIBTOOL) $(LTFLAGS) --mode=uninstall rm -f $(DESTDIR)$(plugindir)/audit_json.la
+ -test -z "$(INSTALL_BACKUP)" || \
+ rm -f $(DESTDIR)$(plugindir)/audit_json.so$(INSTALL_BACKUP)
+
+splint:
+ splint $(SPLINT_OPTS) -I$(incdir) -I$(top_builddir) $(srcdir)/*.c
+
+cppcheck:
+ cppcheck $(CPPCHECK_OPTS) -I$(incdir) -I$(top_builddir) $(srcdir)/*.c
+
+pvs-log-files: $(POBJS)
+
+pvs-studio: $(POBJS)
+ plog-converter $(PVS_LOG_OPTS) $(POBJS)
+
+fuzz:
+
+check-fuzzer:
+
+check: check-fuzzer
+
+check-verbose: check
+
+clean:
+ -$(LIBTOOL) $(LTFLAGS) --mode=clean rm -f *.lo *.o *.la *.a
+ -rm -f *.i *.plog stamp-* core *.core core.*
+
+mostlyclean: clean
+
+distclean: clean
+ -rm -rf Makefile .libs $(shlib_map) $(shlib_opt)
+
+clobber: distclean
+
+realclean: distclean
+ rm -f TAGS tags
+
+cleandir: realclean
+
+.PHONY: clean mostlyclean distclean cleandir clobber realclean
+
+# Autogenerated dependencies, do not modify
+getgrent.lo: $(srcdir)/getgrent.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/getgrent.c
+getgrent.i: $(srcdir)/getgrent.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+getgrent.plog: getgrent.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/getgrent.c --i-file $< --output-file $@
+audit_json.lo: $(srcdir)/audit_json.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_plugin.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/audit_json.c
+audit_json.i: $(srcdir)/audit_json.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_plugin.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+audit_json.plog: audit_json.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/audit_json.c --i-file $< --output-file $@
diff --git a/plugins/audit_json/audit_json.c b/plugins/audit_json/audit_json.c
new file mode 100644
index 0000000..c19c29b
--- /dev/null
+++ b/plugins/audit_json/audit_json.c
@@ -0,0 +1,737 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020-2021 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 <pathnames.h>
+#include <sudo_compat.h>
+#include <sudo_conf.h>
+#include <sudo_debug.h>
+#include <sudo_dso.h>
+#include <sudo_fatal.h>
+#include <sudo_gettext.h>
+#include <sudo_json.h>
+#include <sudo_plugin.h>
+#include <sudo_util.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. */
+const 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, -1);
+ 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 *jsonc, 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[0]) {
+ case '0':
+ if (cp[1] == '\0') {
+ /* Only treat a plain "0" as number 0. */
+ json_value.u.number = 0;
+ json_value.type = JSON_NUMBER;
+ }
+ break;
+ case '+': case '-':
+ if (cp[1] == '0') {
+ /* Encode octal numbers as strings. */
+ break;
+ }
+ FALLTHROUGH;
+ 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(jsonc, name, &json_value));
+}
+
+static bool
+add_array(struct json_container *jsonc, 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(jsonc, name))
+ debug_return_bool(false);
+ while ((cp = *array) != NULL) {
+ json_value.type = JSON_STRING;
+ json_value.u.string = cp;
+ if (!sudo_json_add_value(jsonc, name, &json_value))
+ debug_return_bool(false);
+ array++;
+ }
+ if (!sudo_json_close_array(jsonc))
+ debug_return_bool(false);
+
+ debug_return_bool(true);
+}
+
+static bool
+filter_key_value(const char *kv, const char * const * filter)
+{
+ const 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 *jsonc, const char *name,
+ char * const * array, const 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(jsonc, name))
+ goto bad;
+ for (cur = array; (cp = *cur) != NULL; cur++) {
+ if (filter_key_value(cp, filter))
+ continue;
+ if (!add_key_value(jsonc, cp))
+ goto bad;
+ }
+ if (!sudo_json_close_object(jsonc))
+ goto bad;
+ }
+
+ debug_return_bool(true);
+bad:
+ debug_return_bool(false);
+}
+
+static bool
+add_timestamp(struct json_container *jsonc, struct timespec *ts)
+{
+ struct json_value json_value;
+ time_t secs = ts->tv_sec;
+ char timebuf[1024];
+ struct tm gmt;
+ size_t len;
+ debug_decl(add_timestamp, SUDO_DEBUG_PLUGIN);
+
+ if (gmtime_r(&secs, &gmt) == NULL)
+ debug_return_bool(false);
+
+ sudo_json_open_object(jsonc, "timestamp");
+
+ json_value.type = JSON_NUMBER;
+ json_value.u.number = ts->tv_sec;
+ sudo_json_add_value(jsonc, "seconds", &json_value);
+
+ json_value.type = JSON_NUMBER;
+ json_value.u.number = ts->tv_nsec;
+ sudo_json_add_value(jsonc, "nanoseconds", &json_value);
+
+ timebuf[sizeof(timebuf) - 1] = '\0';
+ len = strftime(timebuf, sizeof(timebuf), "%Y%m%d%H%M%SZ", &gmt);
+ if (len != 0 && timebuf[sizeof(timebuf) - 1] == '\0'){
+ json_value.type = JSON_STRING;
+ json_value.u.string = timebuf;
+ sudo_json_add_value(jsonc, "iso8601", &json_value);
+ }
+
+ timebuf[sizeof(timebuf) - 1] = '\0';
+ len = strftime(timebuf, sizeof(timebuf), "%a %b %e %H:%M:%S %Z %Y", &gmt);
+ if (len != 0 && timebuf[sizeof(timebuf) - 1] == '\0'){
+ json_value.type = JSON_STRING;
+ json_value.u.string = timebuf;
+ sudo_json_add_value(jsonc, "localtime", &json_value);
+ }
+
+ sudo_json_close_object(jsonc);
+
+ debug_return_bool(true);
+}
+
+static int
+audit_write_json(struct json_container * restrict jsonc)
+{
+ 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(jsonc), 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 jsonc;
+ 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("%s", U_("unable to read the clock"));
+ goto done;
+ }
+
+ if (!sudo_json_init(&jsonc, 4, false, false, false))
+ goto oom;
+ if (!sudo_json_open_object(&jsonc, "exit"))
+ goto oom;
+
+ /* Write UUID */
+ json_value.type = JSON_STRING;
+ json_value.u.string = state.uuid_str;
+ if (!sudo_json_add_value(&jsonc, "uuid", &json_value))
+ goto oom;
+
+ /* Write time stamp */
+ if (!add_timestamp(&jsonc, &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(&jsonc, "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(&jsonc, "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(&jsonc, "signal", &json_value))
+ goto oom;
+ } else {
+ json_value.type = JSON_STRING;
+ json_value.u.string = signame; // -V507
+ if (!sudo_json_add_value(&jsonc, "signal", &json_value))
+ goto oom;
+ }
+ /* Core dump? */
+ json_value.type = JSON_BOOL;
+ json_value.u.boolean = WCOREDUMP(exit_status);
+ if (!sudo_json_add_value(&jsonc, "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(&jsonc, "exit_value", &json_value))
+ goto oom;
+ }
+ }
+
+ if (!sudo_json_close_object(&jsonc))
+ goto oom;
+
+ ret = audit_write_json(&jsonc);
+ sudo_json_free(&jsonc);
+done:
+ debug_return_int(ret);
+oom:
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ sudo_json_free(&jsonc);
+ 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 jsonc;
+ 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("%s", U_("unable to read the clock"));
+ goto done;
+ }
+
+ if (!sudo_json_init(&jsonc, 4, false, false, false))
+ goto oom;
+ if (!sudo_json_open_object(&jsonc, audit_str))
+ goto oom;
+
+ json_value.type = JSON_STRING;
+ json_value.u.string = plugin_name;
+ if (!sudo_json_add_value(&jsonc, "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(&jsonc, "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(&jsonc, "reason", &json_value))
+ goto oom;
+ }
+
+ json_value.type = JSON_STRING;
+ json_value.u.string = state.uuid_str;
+ if (!sudo_json_add_value(&jsonc, "uuid", &json_value))
+ goto oom;
+
+ if (!add_timestamp(&jsonc, &now))
+ goto oom;
+
+ /* Write key=value objects. */
+ if (state.settings != NULL) {
+ if (!add_key_value_object(&jsonc, "options", state.settings, settings_filter))
+ goto oom;
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "missing settings list");
+ }
+ if (state.user_info != NULL) {
+ if (!add_key_value_object(&jsonc, "user_info", state.user_info, NULL))
+ goto oom;
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "missing user_info list");
+ }
+ if (command_info != NULL) {
+ if (!add_key_value_object(&jsonc, "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(&jsonc, "submit_optind", &json_value))
+ goto oom;
+
+ if (state.submit_argv != NULL) {
+ if (!add_array(&jsonc, "submit_argv", state.submit_argv))
+ goto oom;
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "missing submit_argv array");
+ }
+ if (state.submit_envp != NULL) {
+ if (!add_array(&jsonc, "submit_envp", state.submit_envp))
+ goto oom;
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "missing submit_envp array");
+ }
+ if (run_argv != NULL) {
+ if (!add_array(&jsonc, "run_argv", run_argv))
+ goto oom;
+ }
+ if (run_envp != NULL) {
+ if (!add_array(&jsonc, "run_envp", run_envp))
+ goto oom;
+ }
+
+ if (!sudo_json_close_object(&jsonc))
+ goto oom;
+
+ ret = audit_write_json(&jsonc);
+ sudo_json_free(&jsonc);
+
+done:
+ debug_return_int(ret);
+oom:
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ sudo_json_free(&jsonc);
+ 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);
+}
+
+sudo_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 */
+};
diff --git a/plugins/audit_json/audit_json.exp b/plugins/audit_json/audit_json.exp
new file mode 100644
index 0000000..9015117
--- /dev/null
+++ b/plugins/audit_json/audit_json.exp
@@ -0,0 +1 @@
+audit_json