From 056173e572928808239a3d661ad99e3a5bf400cf Mon Sep 17 00:00:00 2001 From: "Todd C. Miller" Date: Sun, 29 Mar 2020 05:05:08 -0600 Subject: [PATCH] Parse I/O JSON info file in JSON if present. The JSON version includes more information than the original "log" file in the I/O log dir. --- MANIFEST | 1 + doc/sudoreplay.man.in | 11 +- doc/sudoreplay.mdoc.in | 9 +- include/sudo_iolog.h | 1 + lib/iolog/Makefile.in | 23 +- lib/iolog/iolog_json.c | 760 +++++++++++++++++++++++++++++++++++ lib/iolog/iolog_util.c | 113 ++++-- plugins/sudoers/sudoreplay.c | 28 +- 8 files changed, 891 insertions(+), 55 deletions(-) create mode 100644 lib/iolog/iolog_json.c diff --git a/MANIFEST b/MANIFEST index 078bcdcd0..419e5ee45 100644 --- a/MANIFEST +++ b/MANIFEST @@ -99,6 +99,7 @@ install-sh lib/iolog/Makefile.in lib/iolog/hostcheck.c lib/iolog/iolog_fileio.c +lib/iolog/iolog_json.c lib/iolog/iolog_path.c lib/iolog/iolog_util.c lib/iolog/regress/iolog_path/check_iolog_path.c diff --git a/doc/sudoreplay.man.in b/doc/sudoreplay.man.in index bfc1197a0..eaa9b9fca 100644 --- a/doc/sudoreplay.man.in +++ b/doc/sudoreplay.man.in @@ -2,7 +2,7 @@ .\" .\" SPDX-License-Identifier: ISC .\" -.\" Copyright (c) 2009-2019 Todd C. Miller +.\" Copyright (c) 2009-2020 Todd C. Miller .\" .\" Permission to use, copy, modify, and distribute this software for any .\" purpose with or without fee is hereby granted, provided that the above @@ -16,7 +16,7 @@ .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.TH "SUDOREPLAY" "@mansectsu@" "August 27, 2019" "Sudo @PACKAGE_VERSION@" "System Manager's Manual" +.TH "SUDOREPLAY" "@mansectsu@" "March 26, 2019" "Sudo @PACKAGE_VERSION@" "System Manager's Manual" .nh .if n .ad l .SH "NAME" @@ -183,6 +183,10 @@ was explicitly specified when \fBsudo\fR was run this field will be empty in the log. .TP 8n +host \fIhostname\fR +Evaluates to true if the command was run on the specified +\fIhostname\fR. +.TP 8n runas \fIrunas_user\fR Evaluates to true if the command was run as the specified \fIrunas_user\fR. @@ -396,6 +400,9 @@ The default I/O log directory. \fI@iolog_dir@/00/00/01/log\fR Example session log info. .TP 26n +\fI@iolog_dir@/00/00/01/log.json\fR +Example session log info (JSON format). +.TP 26n \fI@iolog_dir@/00/00/01/stdin\fR Example session standard input log. .TP 26n diff --git a/doc/sudoreplay.mdoc.in b/doc/sudoreplay.mdoc.in index 45376cd5b..d798677d9 100644 --- a/doc/sudoreplay.mdoc.in +++ b/doc/sudoreplay.mdoc.in @@ -1,7 +1,7 @@ .\" .\" SPDX-License-Identifier: ISC .\" -.\" Copyright (c) 2009-2019 Todd C. Miller +.\" Copyright (c) 2009-2020 Todd C. Miller .\" .\" Permission to use, copy, modify, and distribute this software for any .\" purpose with or without fee is hereby granted, provided that the above @@ -15,7 +15,7 @@ .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd August 27, 2019 +.Dd March 26, 2019 .Dt SUDOREPLAY @mansectsu@ .Os Sudo @PACKAGE_VERSION@ .Sh NAME @@ -168,6 +168,9 @@ Note that unless a was explicitly specified when .Nm sudo was run this field will be empty in the log. +.It host Ar hostname +Evaluates to true if the command was run on the specified +.Ar hostname . .It runas Ar runas_user Evaluates to true if the command was run as the specified .Ar runas_user . @@ -358,6 +361,8 @@ Debugging framework configuration The default I/O log directory. .It Pa @iolog_dir@/00/00/01/log Example session log info. +.It Pa @iolog_dir@/00/00/01/log.json +Example session log info (JSON format). .It Pa @iolog_dir@/00/00/01/stdin Example session standard input log. .It Pa @iolog_dir@/00/00/01/stdout diff --git a/include/sudo_iolog.h b/include/sudo_iolog.h index 90a65cc1c..661308d35 100644 --- a/include/sudo_iolog.h +++ b/include/sudo_iolog.h @@ -115,6 +115,7 @@ bool iolog_parse_timing(const char *line, struct timing_closure *timing); char *iolog_parse_delay(const char *cp, struct timespec *delay, const char *decimal_point); int iolog_read_timing_record(struct iolog_file *iol, struct timing_closure *timing); struct iolog_info *iolog_parse_loginfo(int dfd, const char *iolog_dir); +bool iolog_parse_loginfo_json(FILE *fp, const char *iolog_dir, struct iolog_info *li); void iolog_adjust_delay(struct timespec *delay, struct timespec *max_delay, double scale_factor); void iolog_free_loginfo(struct iolog_info *li); diff --git a/lib/iolog/Makefile.in b/lib/iolog/Makefile.in index bf9d0bd8e..9c458b4ed 100644 --- a/lib/iolog/Makefile.in +++ b/lib/iolog/Makefile.in @@ -1,7 +1,7 @@ # # SPDX-License-Identifier: ISC # -# Copyright (c) 2011-2019 Todd C. Miller +# Copyright (c) 2011-2020 Todd C. Miller # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -86,7 +86,8 @@ DEVEL = @DEVEL@ SHELL = @SHELL@ -LIBIOLOG_OBJS = iolog_fileio.lo iolog_path.lo iolog_util.lo hostcheck.lo +LIBIOLOG_OBJS = iolog_fileio.lo iolog_json.lo iolog_path.lo iolog_util.lo \ + hostcheck.lo IOBJS = $(LIBIOLOG_OBJS:.lo=.i) @@ -94,7 +95,7 @@ POBJS = $(IOBJS:.i=.plog) CHECK_IOLOG_PATH_OBJS = check_iolog_path.lo iolog_path.lo -CHECK_IOLOG_UTIL_OBJS = check_iolog_util.lo iolog_util.lo +CHECK_IOLOG_UTIL_OBJS = check_iolog_util.lo iolog_json.lo iolog_util.lo all: libsudo_iolog.la @@ -235,6 +236,22 @@ iolog_fileio.i: $(srcdir)/iolog_fileio.c $(incdir)/compat/stdbool.h \ $(CC) -E -o $@ $(CPPFLAGS) $< iolog_fileio.plog: iolog_fileio.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_fileio.c --i-file $< --output-file $@ +iolog_json.lo: $(srcdir)/iolog_json.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_iolog.h $(incdir)/sudo_json.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(top_builddir)/config.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_json.c +iolog_json.i: $(srcdir)/iolog_json.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_iolog.h $(incdir)/sudo_json.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +iolog_json.plog: iolog_json.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_json.c --i-file $< --output-file $@ iolog_path.lo: $(srcdir)/iolog_path.c $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ diff --git a/lib/iolog/iolog_json.c b/lib/iolog/iolog_json.c new file mode 100644 index 000000000..4372d0b20 --- /dev/null +++ b/lib/iolog/iolog_json.c @@ -0,0 +1,760 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2020 Todd C. Miller + * + * 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 + +#include +#include +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#ifdef HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#ifdef HAVE_STRINGS_H +# include +#endif /* HAVE_STRINGS_H */ +#include +#include +#include +#include +#include +#include + +#include "sudo_gettext.h" /* must be included before sudo_compat.h */ + +#include "sudo_compat.h" +#include "sudo_fatal.h" +#include "sudo_debug.h" +#include "sudo_queue.h" +#include "sudo_json.h" +#include "sudo_util.h" +#include "sudo_iolog.h" + +TAILQ_HEAD(json_item_list, json_item); + +struct json_object { + struct json_item *parent; + struct json_item_list items; +}; + +struct json_item { + TAILQ_ENTRY(json_item) entries; + char *name; /* may be NULL for first brace */ + unsigned int lineno; + enum json_value_type type; + union { + struct json_object child; + char *string; + long long number; + id_t id; + bool boolean; + } u; +}; + +struct json_stack { + unsigned int depth; + unsigned int maxdepth; + struct json_object *frames[64]; +}; +#define JSON_STACK_INTIALIZER(s) { 0, nitems((s).frames) }; + +static bool +json_store_columns(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_columns, SUDO_DEBUG_UTIL); + + if (item->u.number < 1 || item->u.number > INT_MAX) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "tty cols %lld: out of range", item->u.number); + li->cols = 0; + debug_return_bool(false); + } + + li->cols = item->u.number; + debug_return_bool(true); +} + +static bool +json_store_command(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_command, SUDO_DEBUG_UTIL); + + /* + * Note: struct iolog_info must store command + args. + * We don't have argv yet so we append the args later. + */ + li->cmd = item->u.string; + item->u.string = NULL; + debug_return_bool(true); +} + +static bool +json_store_lines(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_lines, SUDO_DEBUG_UTIL); + + if (item->u.number < 1 || item->u.number > INT_MAX) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "tty lines %lld: out of range", item->u.number); + li->lines = 0; + debug_return_bool(false); + } + + li->lines = item->u.number; + debug_return_bool(true); +} + +static char ** +json_array_to_strvec(struct json_object *array) +{ + struct json_item *item; + int len = 0; + char **ret; + debug_decl(json_array_to_strvec, SUDO_DEBUG_UTIL); + + TAILQ_FOREACH(item, &array->items, entries) { + if (item->type != JSON_STRING) { + sudo_warnx(U_("expected JSON_STRING, got %d"), item->type); + debug_return_ptr(NULL); + } + len++; + } + if ((ret = reallocarray(NULL, len + 1, sizeof(char *))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_ptr(NULL); + } + len = 0; + TAILQ_FOREACH(item, &array->items, entries) { + ret[len++] = item->u.string; + item->u.string = NULL; + } + ret[len] = NULL; + + debug_return_ptr(ret); +} + +static bool +json_store_runargv(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_runargv, SUDO_DEBUG_UTIL); + + li->argv = json_array_to_strvec(&item->u.child); + + debug_return_bool(li->argv != NULL); +} + +static bool +json_store_runenv(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_runenv, SUDO_DEBUG_UTIL); + + li->envp = json_array_to_strvec(&item->u.child); + + debug_return_bool(li->envp != NULL); +} + +static bool +json_store_rungid(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_rungid, SUDO_DEBUG_UTIL); + + li->runas_gid = (gid_t)item->u.number; + debug_return_bool(true); +} + +static bool +json_store_rungroup(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_rungroup, SUDO_DEBUG_UTIL); + + li->runas_group = item->u.string; + item->u.string = NULL; + debug_return_bool(true); +} + +static bool +json_store_runuid(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_runuid, SUDO_DEBUG_UTIL); + + li->runas_uid = (uid_t)item->u.number; + debug_return_bool(true); +} + +static bool +json_store_runuser(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_runuser, SUDO_DEBUG_UTIL); + + li->runas_user = item->u.string; + item->u.string = NULL; + debug_return_bool(true); +} + +static bool +json_store_submitcwd(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_submitcwd, SUDO_DEBUG_UTIL); + + li->cwd = item->u.string; + item->u.string = NULL; + debug_return_bool(true); +} + +static bool +json_store_submithost(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_submithost, SUDO_DEBUG_UTIL); + + li->host = item->u.string; + item->u.string = NULL; + debug_return_bool(true); +} + +static bool +json_store_submituser(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_submituser, SUDO_DEBUG_UTIL); + + li->user = item->u.string; + item->u.string = NULL; + debug_return_bool(true); +} + +static bool +json_store_timestamp(struct json_item *item, struct iolog_info *li) +{ + struct json_object *object; + debug_decl(json_store_timestamp, SUDO_DEBUG_UTIL); + + object = &item->u.child; + TAILQ_FOREACH(item, &object->items, entries) { + if (item->type != JSON_NUMBER) + continue; + if (strcmp(item->name, "seconds") == 0) { + li->tstamp.tv_sec = item->u.number; + continue; + } + if (strcmp(item->name, "nanoseconds") == 0) { + li->tstamp.tv_nsec = item->u.number; + continue; + } + } + debug_return_bool(true); +} + +static bool +json_store_ttyname(struct json_item *item, struct iolog_info *li) +{ + debug_decl(json_store_ttyname, SUDO_DEBUG_UTIL); + + li->tty = item->u.string; + item->u.string = NULL; + debug_return_bool(true); +} + +static struct iolog_json_key { + const char *name; + enum json_value_type type; + bool (*setter)(struct json_item *, struct iolog_info *); +} iolog_json_keys[] = { + { "columns", JSON_NUMBER, json_store_columns }, + { "command", JSON_STRING, json_store_command }, + { "lines", JSON_NUMBER, json_store_lines }, + { "runargv", JSON_ARRAY, json_store_runargv }, + { "runenv", JSON_ARRAY, json_store_runenv }, + { "rungid", JSON_ID, json_store_rungid }, + { "rungroup", JSON_STRING, json_store_rungroup }, + { "runuid", JSON_ID, json_store_runuid }, + { "runuser", JSON_STRING, json_store_runuser }, + { "submitcwd", JSON_STRING, json_store_submitcwd }, + { "submithost", JSON_STRING, json_store_submithost }, + { "submituser", JSON_STRING, json_store_submituser }, + { "timestamp", JSON_OBJECT, json_store_timestamp }, + { "ttyname", JSON_STRING, json_store_ttyname }, + { NULL } +}; + +static struct json_item * +new_json_item(enum json_value_type type, char *name, unsigned int lineno) +{ + struct json_item *item; + debug_decl(new_json_item, SUDO_DEBUG_UTIL); + + if ((item = malloc(sizeof(*item))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + debug_return_ptr(NULL); + } + item->name = name; + item->type = type; + item->lineno = lineno; + + debug_return_ptr(item); +} + +static char * +json_parse_string(char **strp) +{ + char *dst, *end, *ret, *src = *strp + 1; + size_t len; + debug_decl(json_parse_string, SUDO_DEBUG_UTIL); + + for (end = src; *end != '"' && *end != '\0'; end++) { + if (end[0] == '\\' && end[1] == '"') + end++; + } + if (*end != '"') { + sudo_warnx(U_("missing double quote in name")); + debug_return_str(NULL); + } + len = (size_t)(end - src); + + /* Copy string, flattening escaped chars. */ + dst = ret = malloc(len + 1); + if (ret == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + while (src < end) { + char ch = *src++; + /* TODO: handle unicode escapes */ + if (ch == '\\') { + switch (*src) { + case 'b': + ch = '\b'; + break; + case 'f': + ch = '\f'; + break; + case 'n': + ch = '\n'; + break; + case 'r': + ch = '\r'; + break; + case 't': + ch = '\t'; + break; + case '"': + case '\\': + default: + /* Note: a bare \ at the end of a string will be removed. */ + ch = *src; + break; + } + src++; + } + *dst++ = ch; + } + *dst = '\0'; + + /* Trim trailing whitespace. */ + do { + end++; + } while (isspace((unsigned char)*end)); + *strp = end; + + debug_return_str(ret); +} + +static void +free_json_items(struct json_item_list *items) +{ + struct json_item *item; + debug_decl(free_json_items, SUDO_DEBUG_UTIL); + + while ((item = TAILQ_FIRST(items)) != NULL) { + TAILQ_REMOVE(items, item, entries); + switch (item->type) { + case JSON_STRING: + free(item->u.string); + break; + case JSON_ARRAY: + case JSON_OBJECT: + free_json_items(&item->u.child.items); + break; + default: + break; + } + free(item->name); + free(item); + } + + debug_return; +} + +static bool +iolog_parse_json_object(struct json_object *object, struct iolog_info *li) +{ + struct json_item *item; + bool ret = false; + debug_decl(iolog_parse_json_object, SUDO_DEBUG_UTIL); + + /* First object holds all the actual data. */ + item = TAILQ_FIRST(&object->items); + if (item->type != JSON_OBJECT) { + sudo_warnx(U_("expected JSON_OBJECT, got %d"), item->type); + goto done; + } + object = &item->u.child; + + TAILQ_FOREACH(item, &object->items, entries) { + struct iolog_json_key *key; + + /* lookup name */ + for (key = iolog_json_keys; key->name != NULL; key++) { + if (strcmp(item->name, key->name) == 0) + break; + } + if (key->name == NULL) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "%s: unknown key %s", __func__, item->name); + } else if (key->type != item->type && + (key->type != JSON_ID || item->type != JSON_NUMBER)) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "%s: key mismatch %s type %d, expected %d", __func__, + item->name, item->type, key->type); + goto done; + } else { + /* Matched name and type. */ + if (!key->setter(item, li)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to store %s", key->name); + goto done; + } + } + } + + /* Merge cmd and argv as sudoreplay expects. */ + if (li->cmd != NULL && li->argv != NULL) { + size_t len = strlen(li->cmd) + 1; + char *newcmd; + int ac; + + /* Skip argv[0], we use li->cmd instead. */ + for (ac = 1; li->argv[ac] != NULL; ac++) + len += strlen(li->argv[ac]) + 1; + + if ((newcmd = malloc(len)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + + /* TODO: optimize this. */ + if (strlcpy(newcmd, li->cmd, len) >= len) + sudo_fatalx(U_("internal error, %s overflow"), __func__); + for (ac = 1; li->argv[ac] != NULL; ac++) { + if (strlcat(newcmd, " ", len) >= len) + sudo_fatalx(U_("internal error, %s overflow"), __func__); + if (strlcat(newcmd, li->argv[ac], len) >= len) + sudo_fatalx(U_("internal error, %s overflow"), __func__); + } + + free(li->cmd); + li->cmd = newcmd; + } + + ret = true; + +done: + debug_return_bool(ret); +} + +static bool +json_insert_bool(struct json_item_list *items, char *name, bool value, + unsigned int lineno) +{ + struct json_item *item; + debug_decl(json_insert_bool, SUDO_DEBUG_UTIL); + + if ((item = new_json_item(JSON_BOOL, name, lineno)) == NULL) + debug_return_bool(false); + item->u.boolean = value; + TAILQ_INSERT_TAIL(items, item, entries); + + debug_return_bool(true); +} + +static bool +json_insert_num(struct json_item_list *items, char *name, long long value, + unsigned int lineno) +{ + struct json_item *item; + debug_decl(json_insert_num, SUDO_DEBUG_UTIL); + + if ((item = new_json_item(JSON_NUMBER, name, lineno)) == NULL) + debug_return_bool(false); + item->u.number = value; + TAILQ_INSERT_TAIL(items, item, entries); + + debug_return_bool(true); +} + +static bool +json_insert_str(struct json_item_list *items, char *name, char **strp, + unsigned int lineno) +{ + struct json_item *item; + debug_decl(json_insert_str, SUDO_DEBUG_UTIL); + + if ((item = new_json_item(JSON_STRING, name, lineno)) == NULL) + debug_return_bool(false); + item->u.string = json_parse_string(strp); + if (item->u.string == NULL) { + free(item); + debug_return_bool(false); + } + TAILQ_INSERT_TAIL(items, item, entries); + + debug_return_bool(true); +} + +static struct json_object * +json_stack_push(struct json_stack *stack, struct json_item_list *items, + struct json_object *frame, enum json_value_type type, char *name, + unsigned int lineno) +{ + struct json_item *item; + debug_decl(iolog_parse_loginfo_json, SUDO_DEBUG_UTIL); + + /* Allocate a new item and insert it into the list. */ + if ((item = new_json_item(type, name, lineno)) == NULL) + debug_return_ptr(NULL); + TAILQ_INIT(&item->u.child.items); + item->u.child.parent = item; + TAILQ_INSERT_TAIL(items, item, entries); + + /* Push the current frame onto the stack. */ + if (stack->depth == stack->maxdepth) + sudo_fatalx(U_("internal error, %s overflow"), __func__); + stack->frames[stack->depth++] = frame; + + /* Return the new frame */ + debug_return_ptr(&item->u.child); +} + +bool +iolog_parse_loginfo_json(FILE *fp, const char *iolog_dir, struct iolog_info *li) +{ + struct json_object root = { NULL, TAILQ_HEAD_INITIALIZER(root.items) }; + struct json_object *curobj = &root; + struct json_object *curarray = NULL; + struct json_stack objstack = JSON_STACK_INTIALIZER(objstack); + struct json_stack arrstack = JSON_STACK_INTIALIZER(arrstack); + unsigned int lineno = 0; + char *name = NULL; + char *buf = NULL; + size_t bufsize = 0; + ssize_t len; + long long num; + bool ret = false; + debug_decl(iolog_parse_loginfo_json, SUDO_DEBUG_UTIL); + + while ((len = getdelim(&buf, &bufsize, '\n', fp)) != -1) { + char *cp = buf; + char *ep = buf + len - 1; + + lineno++; + + /* Trim trailing whitespace. */ + while (ep > cp && isspace((unsigned char)*ep)) + ep--; + ep[1] = '\0'; + + for (;;) { + const char *errstr; + + /* Trim leading whitespace, skip blank lines. */ + while (isspace((unsigned char)*cp)) + cp++; + + /* Strip out commas. TODO: require commas between values. */ + if (*cp == ',') { + cp++; + while (isspace((unsigned char)*cp)) + cp++; + } + + if (*cp == '\0') + break; + + switch (*cp) { + case '{': + cp++; + curobj = json_stack_push(&objstack, &curobj->items, curobj, + JSON_OBJECT, name, lineno); + if (curobj == NULL) + goto parse_error; + name = NULL; + break; + case '}': + cp++; + if (curobj->parent == NULL || curobj->parent->type != JSON_OBJECT) { + sudo_warnx(U_("unmatched close brace")); + goto parse_error; + } + curobj = objstack.frames[--objstack.depth]; + break; + case '[': + cp++; + if (curobj->parent == NULL) { + /* Must have an enclosing object. */ + sudo_warnx(U_("unexpected array")); + goto parse_error; + } + curarray = json_stack_push(&arrstack, &curobj->items, curarray, + JSON_ARRAY, name, lineno); + if (curarray == NULL) + goto parse_error; + name = NULL; + break; + case ']': + cp++; + if (curarray == NULL || curarray->parent == NULL || + curarray->parent->type != JSON_ARRAY) { + sudo_warnx(U_("unmatched close bracket")); + goto parse_error; + } + curarray = arrstack.frames[--arrstack.depth]; + break; + case '"': + if (curobj->parent == NULL) { + /* Must have an enclosing object. */ + sudo_warnx(U_("unexpected string")); + goto parse_error; + } + + if (curarray != NULL) { + if (!json_insert_str(&curarray->items, NULL, &cp, lineno)) + goto parse_error; + } else if (name != NULL) { + if (!json_insert_str(&curobj->items, name, &cp, lineno)) + goto parse_error; + name = NULL; + } else { + /* Parsing "name": */ + if ((name = json_parse_string(&cp)) == NULL) + goto parse_error; + /* TODO: allow colon on next line? */ + if (*cp++ != ':') { + sudo_warnx(U_("missing colon after name")); + goto parse_error; + } + } + break; + case 't': + if (name == NULL) { + sudo_warnx(U_("unexpected boolean")); + goto parse_error; + } + if (strcmp(cp, "true") != 0) + goto parse_error; + cp += sizeof("true") - 1; + + if (curarray != NULL) { + if (!json_insert_bool(&curarray->items, NULL, true, lineno)) + goto parse_error; + } else { + if (!json_insert_bool(&curobj->items, name, true, lineno)) + goto parse_error; + name = NULL; + } + break; + case 'f': + if (name == NULL) { + sudo_warnx(U_("unexpected boolean")); + goto parse_error; + } + if (strcmp(cp, "false") != 0) + goto parse_error; + cp += sizeof("false") - 1; + + if (curarray != NULL) { + if (!json_insert_bool(&curarray->items, NULL, false, lineno)) + goto parse_error; + } else { + if (!json_insert_bool(&curobj->items, name, false, lineno)) + goto parse_error; + name = NULL; + } + break; + case 'n': + if (strcmp(cp, "null") == 0) + sudo_warnx(U_("null not allowed")); + goto parse_error; + case '+': case '-': case '0': case '1': case '2': case '3': + case '4': case '5': case '6': case '7': case '8': case '9': + if (name == NULL) { + sudo_warnx(U_("unexpected number")); + goto parse_error; + } + len = strcspn(cp, " \t\r\n,"); + cp[len] = '\0'; + num = sudo_strtonum(cp, LLONG_MIN, LLONG_MAX, &errstr); + if (errstr != NULL) { + sudo_warnx(U_("%s: %s"), cp, U_(errstr)); + goto parse_error; + } + cp += len; + + if (curarray != NULL) { + if (!json_insert_num(&curarray->items, NULL, num, lineno)) + goto parse_error; + } else { + if (!json_insert_num(&curobj->items, name, num, lineno)) + goto parse_error; + name = NULL; + } + break; + default: + goto parse_error; + } + } + } + if (objstack.depth != 0) { + sudo_warnx(U_("unmatched close brace")); + goto parse_error; + } + if (arrstack.depth != 0) { + sudo_warnx(U_("unmatched close bracket")); + goto parse_error; + } + + /* Walk the stack and parse entries. */ + ret = iolog_parse_json_object(&root, li); + + goto done; + +parse_error: + sudo_warnx(U_("%s/%s:%u unable to parse \"%s\""), iolog_dir, + "log.json", lineno, buf); +done: + free(buf); + free(name); + free_json_items(&root.items); + + debug_return_bool(ret); +} diff --git a/lib/iolog/iolog_util.c b/lib/iolog/iolog_util.c index f3bae0275..85cdcf006 100644 --- a/lib/iolog/iolog_util.c +++ b/lib/iolog/iolog_util.c @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: ISC * - * Copyright (c) 2009-2019 Todd C. Miller + * Copyright (c) 2009-2020 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -31,11 +31,6 @@ #else # include "compat/stdbool.h" #endif /* HAVE_STDBOOL_H */ -#if defined(HAVE_STDINT_H) -# include -#elif defined(HAVE_INTTYPES_H) -# include -#endif #ifdef HAVE_STRING_H # include #endif /* HAVE_STRING_H */ @@ -60,32 +55,15 @@ static int timing_event_adj; -struct iolog_info * -iolog_parse_loginfo(int dfd, const char *iolog_dir) +static bool +iolog_parse_loginfo_legacy(FILE *fp, const char *iolog_dir, + struct iolog_info *li) { char *buf = NULL, *cp, *ep; const char *errstr; size_t bufsize = 0, cwdsize = 0, cmdsize = 0; - struct iolog_info *li = NULL; - FILE *fp = NULL; - int fd = -1; - debug_decl(iolog_parse_loginfo, SUDO_DEBUG_UTIL); - - if (dfd == -1) { - if ((dfd = open(iolog_dir, O_RDONLY)) == -1) { - sudo_warn("%s", iolog_dir); - goto bad; - } - fd = openat(dfd, "log", O_RDONLY, 0); - close(dfd); - } else { - fd = openat(dfd, "log", O_RDONLY, 0); - } - if (fd == -1 || (fp = fdopen(fd, "r")) == NULL) { - sudo_warn("%s/log", iolog_dir); - goto bad; - } - fd = -1; + bool ret = false; + debug_decl(iolog_parse_loginfo_legacy, SUDO_DEBUG_UTIL); /* * Info file has three lines: @@ -93,18 +71,12 @@ iolog_parse_loginfo(int dfd, const char *iolog_dir) * 2) cwd * 3) command with args */ - if ((li = calloc(1, sizeof(*li))) == NULL) - sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); - li->runas_uid = (uid_t)-1; - li->runas_gid = (gid_t)-1; if (getdelim(&buf, &bufsize, '\n', fp) == -1 || getdelim(&li->cwd, &cwdsize, '\n', fp) == -1 || getdelim(&li->cmd, &cmdsize, '\n', fp) == -1) { sudo_warn(U_("%s: invalid log file"), iolog_dir); - goto bad; + goto done; } - fclose(fp); - fp = NULL; /* Strip the newline from the cwd and command. */ li->cwd[strcspn(li->cwd, "\n")] = '\0'; @@ -121,20 +93,20 @@ iolog_parse_loginfo(int dfd, const char *iolog_dir) /* timestamp */ if ((ep = strchr(cp, ':')) == NULL) { sudo_warn(U_("%s: time stamp field is missing"), iolog_dir); - goto bad; + goto done; } *ep = '\0'; li->tstamp.tv_sec = sudo_strtonum(cp, 0, TIME_T_MAX, &errstr); if (errstr != NULL) { sudo_warn(U_("%s: time stamp %s: %s"), iolog_dir, cp, errstr); - goto bad; + goto done; } /* submit user */ cp = ep + 1; if ((ep = strchr(cp, ':')) == NULL) { sudo_warn(U_("%s: user field is missing"), iolog_dir); - goto bad; + goto done; } if ((li->user = strndup(cp, (size_t)(ep - cp))) == NULL) sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); @@ -143,7 +115,7 @@ iolog_parse_loginfo(int dfd, const char *iolog_dir) cp = ep + 1; if ((ep = strchr(cp, ':')) == NULL) { sudo_warn(U_("%s: runas user field is missing"), iolog_dir); - goto bad; + goto done; } if ((li->runas_user = strndup(cp, (size_t)(ep - cp))) == NULL) sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); @@ -152,7 +124,7 @@ iolog_parse_loginfo(int dfd, const char *iolog_dir) cp = ep + 1; if ((ep = strchr(cp, ':')) == NULL) { sudo_warn(U_("%s: runas group field is missing"), iolog_dir); - goto bad; + goto done; } if (cp != ep) { if ((li->runas_group = strndup(cp, (size_t)(ep - cp))) == NULL) @@ -189,15 +161,60 @@ iolog_parse_loginfo(int dfd, const char *iolog_dir) } } } + + ret = true; + +done: free(buf); - debug_return_ptr(li); + debug_return_bool(ret); +} + +struct iolog_info * +iolog_parse_loginfo(int dfd, const char *iolog_dir) +{ + struct iolog_info *li = NULL; + FILE *fp = NULL; + int fd = -1; + int tmpfd = -1; + bool ok, legacy = false; + debug_decl(iolog_parse_loginfo, SUDO_DEBUG_UTIL); + + if (dfd == -1) { + if ((tmpfd = open(iolog_dir, O_RDONLY)) == -1) { + sudo_warn("%s", iolog_dir); + goto bad; + } + dfd = tmpfd; + } + if ((fd = openat(dfd, "log.json", O_RDONLY, 0)) == -1) { + fd = openat(dfd, "log", O_RDONLY, 0); + legacy = true; + } + if (tmpfd != -1) + close(tmpfd); + if (fd == -1 || (fp = fdopen(fd, "r")) == NULL) { + sudo_warn("%s/log", iolog_dir); + goto bad; + } + fd = -1; + + if ((li = calloc(1, sizeof(*li))) == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + li->runas_uid = (uid_t)-1; + li->runas_gid = (gid_t)-1; + + ok = legacy ? iolog_parse_loginfo_legacy(fp, iolog_dir, li) : + iolog_parse_loginfo_json(fp, iolog_dir, li); + if (ok) { + fclose(fp); + debug_return_ptr(li); + } bad: if (fd != -1) close(fd); if (fp != NULL) fclose(fp); - free(buf); iolog_free_loginfo(li); debug_return_ptr(NULL); } @@ -422,7 +439,19 @@ iolog_read_timing_record(struct iolog_file *iol, struct timing_closure *timing) void iolog_free_loginfo(struct iolog_info *li) { + char **p; + if (li != NULL) { + if (li->argv != NULL) { + for (p = li->argv; *p != NULL; p++) + free(*p); + free(li->argv); + } + if (li->envp != NULL) { + for (p = li->envp; *p != NULL; p++) + free(*p); + free(li->envp); + } free(li->cwd); free(li->user); free(li->runas_user); diff --git a/plugins/sudoers/sudoreplay.c b/plugins/sudoers/sudoreplay.c index 353fa48ea..8a1002e10 100644 --- a/plugins/sudoers/sudoreplay.c +++ b/plugins/sudoers/sudoreplay.c @@ -119,6 +119,7 @@ struct search_node { #define ST_FROMDATE 7 #define ST_TODATE 8 #define ST_CWD 9 +#define ST_HOST 10 char type; bool negated; bool or; @@ -126,6 +127,7 @@ struct search_node { regex_t cmdre; struct timespec tstamp; char *cwd; + char *host; char *tty; char *user; char *runas_group; @@ -199,8 +201,7 @@ static void setup_terminal(struct iolog_info *li, bool interactive, bool resize) isalnum((unsigned char)(s)[3]) && isalnum((unsigned char)(s)[4]) && \ (s)[5] == '/' && \ isalnum((unsigned char)(s)[6]) && isalnum((unsigned char)(s)[7]) && \ - (s)[8] == '/' && (s)[9] == 'l' && (s)[10] == 'o' && (s)[11] == 'g' && \ - (s)[12] == '\0') + (s)[8] == '\0') __dso_public int main(int argc, char *argv[]); @@ -1171,6 +1172,11 @@ parse_expr(struct search_node_list *head, char *argv[], bool sub_expr) goto bad; type = ST_RUNASGROUP; break; + case 'h': /* host */ + if (strncmp(*av, "host", strlen(*av)) != 0) + goto bad; + type = ST_HOST; + break; case 'r': /* runas user */ if (strncmp(*av, "runas", strlen(*av)) != 0) goto bad; @@ -1259,20 +1265,28 @@ match_expr(struct search_node_list *head, struct iolog_info *log, bool last_matc res = match_expr(&sn->u.expr, log, matched); break; case ST_CWD: - res = strcmp(sn->u.cwd, log->cwd) == 0; + if (log->cwd != NULL) + res = strcmp(sn->u.cwd, log->cwd) == 0; + break; + case ST_HOST: + if (log->host != NULL) + res = strcmp(sn->u.host, log->host) == 0; break; case ST_TTY: - res = strcmp(sn->u.tty, log->tty) == 0; + if (log->tty != NULL) + res = strcmp(sn->u.tty, log->tty) == 0; break; case ST_RUNASGROUP: if (log->runas_group != NULL) res = strcmp(sn->u.runas_group, log->runas_group) == 0; break; case ST_RUNASUSER: - res = strcmp(sn->u.runas_user, log->runas_user) == 0; + if (log->runas_user != NULL) + res = strcmp(sn->u.runas_user, log->runas_user) == 0; break; case ST_USER: - res = strcmp(sn->u.user, log->user) == 0; + if (log->user != NULL) + res = strcmp(sn->u.user, log->user) == 0; break; case ST_PATTERN: rc = regexec(&sn->u.cmdre, log->cmd, 0, NULL, 0); @@ -1339,6 +1353,8 @@ list_session(char *log_dir, regex_t *re, const char *user, const char *tty) li->user, li->tty, li->cwd, li->runas_user); if (li->runas_group) printf("GROUP=%s ; ", li->runas_group); + if (li->host) + printf("HOST=%s ; ", li->host); printf("TSID=%s ; COMMAND=%s\n", idstr, li->cmd); ret = 0;