2
0
mirror of https://github.com/sudo-project/sudo.git synced 2025-08-22 01:49:11 +00:00

cvtsudoers: initial support for CSV output

For CSV output we double quotes strings that contain commas.  For
each literal double quote character present inside the string, two
double quotes are output.
This commit is contained in:
Todd C. Miller 2021-10-10 14:24:31 -06:00
parent de5ca187b8
commit 430048d167
7 changed files with 767 additions and 7 deletions

View File

@ -546,6 +546,7 @@ plugins/sudoers/check.h
plugins/sudoers/check_aliases.c
plugins/sudoers/cvtsudoers.c
plugins/sudoers/cvtsudoers.h
plugins/sudoers/cvtsudoers_csv.c
plugins/sudoers/cvtsudoers_json.c
plugins/sudoers/cvtsudoers_ldif.c
plugins/sudoers/cvtsudoers_pwutil.c

View File

@ -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 "CVTSUDOERS" "1" "September 24, 2021" "Sudo @PACKAGE_VERSION@" "General Commands Manual"
.TH "CVTSUDOERS" "1" "October 10, 2021" "Sudo @PACKAGE_VERSION@" "General Commands Manual"
.nh
.if n .ad l
.SH "NAME"
@ -135,6 +135,17 @@ The following formats are supported:
.RS 12n
.PD 0
.TP 10n
CSV
CSV (comma-separated value) files are often used by spreadsheets
and report generators.
For CSV output,
\fBcvtsudoers\fR
double quotes strings that contain commas.
For each literal double quote character present inside the string,
two double quotes are output.
This method of quoting commas is compatible with most spreadsheet programs.
.PD
.TP 10n
JSON
JSON (JavaScript Object Notation) files are usually easier for
third-party applications to consume than the traditional
@ -144,7 +155,6 @@ The various values have explicit types which removes much of the
ambiguity of the
\fIsudoers\fR
format.
.PD
.TP 10n
LDIF
LDIF (LDAP Data Interchange Format) files can be imported into an LDAP
@ -400,7 +410,7 @@ See the description of the
\fB\-O\fR
command line option.
.TP 6n
\fBoutput_format =\fR \fIjson\fR | \fIldif\fR | \fIsudoers\fR
\fBoutput_format =\fR \fIcsv\fR | \fIjson\fR | \fIldif\fR | \fIsudoers\fR
See the description of the
\fB\-f\fR
command line option.

View File

@ -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 September 24, 2021
.Dd October 10, 2021
.Dt CVTSUDOERS 1
.Os Sudo @PACKAGE_VERSION@
.Sh NAME
@ -117,6 +117,15 @@ is JSON or sudoers.
Specify the output format (case-insensitive).
The following formats are supported:
.Bl -tag -width 8n
.It CSV
CSV (comma-separated value) files are often used by spreadsheets
and report generators.
For CSV output,
.Nm
double quotes strings that contain commas.
For each literal double quote character present inside the string,
two double quotes are output.
This method of quoting commas is compatible with most spreadsheet programs.
.It JSON
JSON (JavaScript Object Notation) files are usually easier for
third-party applications to consume than the traditional
@ -342,7 +351,7 @@ command line option.
See the description of the
.Fl O
command line option.
.It Sy output_format = Ar json | ldif | sudoers
.It Sy output_format = Ar csv | json | ldif | sudoers
See the description of the
.Fl f
command line option.

View File

@ -195,7 +195,7 @@ VISUDO_OBJS = check_aliases.o editor.lo find_path.lo gc.lo goodpath.lo \
VISUDO_IOBJS = sudo_printf.i visudo.i
CVTSUDOERS_OBJS = b64_encode.o cvtsudoers.o cvtsudoers_json.o \
CVTSUDOERS_OBJS = b64_encode.o cvtsudoers.o cvtsudoers_json.o cvtsudoers_csv.o \
cvtsudoers_ldif.o cvtsudoers_pwutil.o fmtsudoers.lo \
fmtsudoers_cvt.lo locale.lo parse_ldif.o stubs.o \
sudo_printf.o ldap_util.lo testsudoers_pwutil.o tsgetgrpw.o
@ -1341,6 +1341,32 @@ cvtsudoers.i: $(srcdir)/cvtsudoers.c $(devdir)/def_data.h $(devdir)/gram.h \
$(CC) -E -o $@ $(CPPFLAGS) $<
cvtsudoers.plog: cvtsudoers.i
rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/cvtsudoers.c --i-file $< --output-file $@
cvtsudoers_csv.o: $(srcdir)/cvtsudoers_csv.c $(devdir)/def_data.h \
$(devdir)/gram.h $(incdir)/compat/stdbool.h \
$(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
$(incdir)/sudo_debug.h $(incdir)/sudo_eventlog.h \
$(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
$(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
$(incdir)/sudo_util.h $(srcdir)/cvtsudoers.h \
$(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \
$(srcdir)/strlist.h $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
$(srcdir)/sudoers_debug.h $(top_builddir)/config.h \
$(top_builddir)/pathnames.h
$(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/cvtsudoers_csv.c
cvtsudoers_csv.i: $(srcdir)/cvtsudoers_csv.c $(devdir)/def_data.h \
$(devdir)/gram.h $(incdir)/compat/stdbool.h \
$(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
$(incdir)/sudo_debug.h $(incdir)/sudo_eventlog.h \
$(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
$(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
$(incdir)/sudo_util.h $(srcdir)/cvtsudoers.h \
$(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \
$(srcdir)/strlist.h $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
$(srcdir)/sudoers_debug.h $(top_builddir)/config.h \
$(top_builddir)/pathnames.h
$(CC) -E -o $@ $(CPPFLAGS) $<
cvtsudoers_csv.plog: cvtsudoers_csv.i
rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/cvtsudoers_csv.c --i-file $< --output-file $@
cvtsudoers_json.o: $(srcdir)/cvtsudoers_json.c $(devdir)/def_data.h \
$(devdir)/gram.h $(incdir)/compat/stdbool.h \
$(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \

View File

@ -263,7 +263,10 @@ main(int argc, char *argv[])
}
}
if (conf->output_format != NULL) {
if (strcasecmp(conf->output_format, "json") == 0) {
if (strcasecmp(conf->output_format, "csv") == 0) {
output_format = format_csv;
conf->store_options = true;
} else if (strcasecmp(conf->output_format, "json") == 0) {
output_format = format_json;
conf->store_options = true;
} else if (strcasecmp(conf->output_format, "ldif") == 0) {
@ -376,6 +379,9 @@ main(int argc, char *argv[])
}
switch (output_format) {
case format_csv:
exitcode = !convert_sudoers_csv(&parsed_policy, output_file, conf);
break;
case format_json:
exitcode = !convert_sudoers_json(&parsed_policy, output_file, conf);
break;

View File

@ -23,6 +23,7 @@
/* Supported input/output formats. */
enum sudoers_formats {
format_csv,
format_json,
format_ldif,
format_sudoers
@ -83,6 +84,9 @@ struct cvtsudoers_filter {
/* cvtsudoers.c */
extern struct cvtsudoers_filter *filters;
/* cvtsudoers_csv.c */
bool convert_sudoers_csv(struct sudoers_parse_tree *parse_tree, const char *output_file, struct cvtsudoers_config *conf);
/* cvtsudoers_json.c */
bool convert_sudoers_json(struct sudoers_parse_tree *parse_tree, const char *output_file, struct cvtsudoers_config *conf);

View File

@ -0,0 +1,704 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif /* HAVE_STRINGS_H */
#include <unistd.h>
#include <stdarg.h>
#include "sudoers.h"
#include "cvtsudoers.h"
#include <gram.h>
static void print_member_list_csv(FILE *fp, struct sudoers_parse_tree *parse_tree, struct member_list *members, bool negated, int alias_type, bool expand_aliases);
/*
* Print sudoOptions from a defaults_list.
*/
static bool
print_options_csv(FILE *fp, struct defaults_list *options, bool need_comma)
{
struct defaults *opt;
debug_decl(print_options_csv, SUDOERS_DEBUG_UTIL);
TAILQ_FOREACH(opt, options, entries) {
if (opt->val != NULL) {
/* There is no need to double quote values here. */
fprintf(fp, "%s%s%s%s", need_comma ? "," : "", opt->var,
opt->op == '+' ? "+=" : opt->op == '-' ? "-=" : "=", opt->val);
} else {
/* Boolean flag. */
fprintf(fp, "%s%s%s%s", need_comma ? "," : "", opt->var,
opt->op == false ? "!" : "", opt->var);
}
need_comma = true;
}
debug_return_bool(!ferror(fp));
}
/*
* Map a Defaults type to string.
*/
static const char *
defaults_type_to_string(int defaults_type)
{
switch (defaults_type) {
case DEFAULTS:
return "defaults";
case DEFAULTS_CMND:
return "defaults_command";
case DEFAULTS_HOST:
return "defaults_host";
case DEFAULTS_RUNAS:
return "defaults_runas";
case DEFAULTS_USER:
return "defaults_user";
default:
sudo_fatalx_nodebug("unexpected defaults type %d", defaults_type);
}
}
/*
* Map a Defaults type to an alias type.
*/
static int
defaults_to_alias_type(int defaults_type)
{
switch (defaults_type) {
case DEFAULTS_CMND:
return CMNDALIAS;
case DEFAULTS_HOST:
return HOSTALIAS;
case DEFAULTS_RUNAS:
return RUNASALIAS;
case DEFAULTS_USER:
return USERALIAS;
default:
sudo_fatalx_nodebug("unexpected defaults type %d", defaults_type);
}
}
/*
* Print a string, performing quoting as needed.
* If a field includes a comma it must be double-quoted.
* Double quotes are replaced by a pair of double-quotes.
* XXX - rewrite this
*/
static bool
print_csv_string(FILE *fp, const char *str, bool quoted)
{
const char *src = str;
char *dst, *newstr;
size_t len, newsize;
bool quote_it = false;
bool ret = true;
debug_decl(print_csv_string, SUDOERS_DEBUG_UTIL);
len = strcspn(str, quoted ? "\"" : "\",");
if (str[len] == '\0') {
/* nothing to escape */
debug_return_bool(fputs(str, fp) != EOF);
}
if (!quoted && strchr(str + len, ',') != NULL)
quote_it = true;
/* String includes characters we need to escape. */
newsize = len + 2 + (strlen(len + str) * 2) + 1;
if ((newstr = malloc(newsize)) == NULL)
debug_return_bool(false);
dst = newstr;
if (quote_it)
*dst++ = '"';
while (*src != '\0') {
if (*src == '"')
*dst++ = '"';
*dst++ = *src++;
}
if (quote_it)
*dst++ = '"';
*dst = '\0';
if (fputs(newstr, fp) == EOF)
ret = false;
free(newstr);
debug_return_bool(ret);
}
/*
* Format a sudo_command as a string.
* Returns the formatted, dynamically allocated string or dies on error.
*/
static char *
format_cmnd(struct sudo_command *c, bool negated)
{
struct command_digest *digest;
char *buf, *cp, *cmnd;
size_t bufsiz;
int len;
debug_decl(format_cmnd, SUDOERS_DEBUG_UTIL);
cmnd = c->cmnd ? c->cmnd : "ALL";
bufsiz = negated + strlen(cmnd) + 1;
if (c->args != NULL)
bufsiz += 1 + strlen(c->args);
TAILQ_FOREACH(digest, &c->digests, entries) {
bufsiz += strlen(digest_type_to_name(digest->digest_type)) + 1 +
strlen(digest->digest_str) + 1;
if (TAILQ_NEXT(digest, entries) != NULL)
bufsiz += 2;
}
if ((buf = malloc(bufsiz)) == NULL) {
sudo_fatalx(U_("%s: %s"), __func__,
U_("unable to allocate memory"));
}
cp = buf;
TAILQ_FOREACH(digest, &c->digests, entries) {
len = snprintf(cp, bufsiz - (cp - buf), "%s:%s%s ",
digest_type_to_name(digest->digest_type), digest->digest_str,
TAILQ_NEXT(digest, entries) ? "," : "");
if (len < 0 || len >= (int)bufsiz - (cp - buf))
sudo_fatalx(U_("internal error, %s overflow"), __func__);
cp += len;
}
len = snprintf(cp, bufsiz - (cp - buf), "%s%s%s%s", negated ? "!" : "",
cmnd, c->args ? " " : "", c->args ? c->args : "");
if (len < 0 || len >= (int)bufsiz - (cp - buf))
sudo_fatalx(U_("internal error, %s overflow"), __func__);
debug_return_str(buf);
}
/*
* Print struct member in CSV format as the specified attribute.
* See print_member_int() in parse.c.
*/
static void
print_member_csv(FILE *fp, struct sudoers_parse_tree *parse_tree, char *name,
int type, bool negated, bool quoted, int alias_type, bool expand_aliases)
{
struct alias *a;
char *str;
int len;
debug_decl(print_member_csv, SUDOERS_DEBUG_UTIL);
switch (type) {
case MYSELF:
/* Only valid for sudoRunasUser */
break;
case ALL:
if (name == NULL) {
fputs(negated ? "!ALL" : "ALL", fp);
break;
}
FALLTHROUGH;
case COMMAND:
str = format_cmnd((struct sudo_command *)name, negated);
print_csv_string(fp, str, quoted);
free(str);
break;
case ALIAS:
if (expand_aliases) {
if ((a = alias_get(parse_tree, name, alias_type)) != NULL) {
print_member_list_csv(fp, parse_tree, &a->members, negated,
alias_type, expand_aliases);
alias_put(a);
break;
}
}
FALLTHROUGH;
default:
len = asprintf(&str, "%s%s", negated ? "!" : "", name);
if (len == -1) {
sudo_fatalx(U_("%s: %s"), __func__,
U_("unable to allocate memory"));
}
print_csv_string(fp, str, quoted);
free(str);
break;
}
debug_return;
}
/*
* Print list of struct member in CSV format as the specified attribute.
* See print_member_int() in parse.c.
*/
static void
print_member_list_csv(FILE *fp, struct sudoers_parse_tree *parse_tree,
struct member_list *members, bool negated, int alias_type,
bool expand_aliases)
{
struct member *m, *next;
debug_decl(print_member_list_csv, SUDOERS_DEBUG_UTIL);
if (TAILQ_EMPTY(members))
debug_return;
if (TAILQ_FIRST(members) != TAILQ_LAST(members, member_list))
putc('"', fp);
TAILQ_FOREACH_SAFE(m, members, entries, next) {
print_member_csv(fp, parse_tree, m->name, m->type,
negated ? !m->negated : m->negated, true, alias_type,
expand_aliases);
if (next != NULL)
putc(',', fp);
}
if (TAILQ_FIRST(members) != TAILQ_LAST(members, member_list))
putc('"', fp);
debug_return;
}
/*
* Print the binding for a Defaults entry of the specified type.
*/
static void
print_defaults_binding_csv(FILE *fp, struct sudoers_parse_tree *parse_tree,
struct member_list *binding, int type, bool expand_aliases)
{
int alias_type;
debug_decl(print_defaults_binding_csv, SUDOERS_DEBUG_UTIL);
if (type != DEFAULTS) {
/* Print each member object in binding. */
alias_type = defaults_to_alias_type(type);
print_member_list_csv(fp, parse_tree, binding, false, alias_type,
expand_aliases);
}
debug_return;
}
/*
* Print all Defaults in CSV format:
*
* defaults,binding,name,operator,value
*
* where "operator" is one of +=, -=, or =
* and boolean flags use true/false for the value.
*/
static bool
print_defaults_csv(FILE *fp, struct sudoers_parse_tree *parse_tree,
bool expand_aliases)
{
struct defaults *def;
debug_decl(print_defaults_csv, SUDOERS_DEBUG_UTIL);
if (TAILQ_EMPTY(&parse_tree->defaults))
debug_return_bool(true);
/* Heading line. */
fputs("defaults_type,binding,name,operator,value\n", fp);
TAILQ_FOREACH(def, &parse_tree->defaults, entries) {
const char *operator;
/* Print operator */
switch (def->op) {
case '+':
operator = "+=";
break;
case '-':
operator = "-=";
break;
case true:
case false:
operator = "=";
break;
default:
sudo_warnx("internal error: unexpected defaults op %d", def->op);
continue;
}
/*
* For CSV we use a separate entry for each Defaults setting,
* even if they were on the same line in sudoers.
*/
fprintf(fp, "%s,", defaults_type_to_string(def->type));
/* Print binding (if any), which could be a list. */
print_defaults_binding_csv(fp, parse_tree, def->binding, def->type,
expand_aliases);
/* Print Defaults name + operator. */
fprintf(fp, ",%s,%s,", def->var, operator);
/* Print defaults value. */
/* XXX - differentiate between lists and single values? */
if (def->val == NULL) {
fputs(def->op == true ? "true" : "false", fp);
} else {
/* Does not handle lists specially. */
print_csv_string(fp, def->val, false);
}
putc('\n', fp);
}
putc('\n', fp);
fflush(fp);
debug_return_bool(!ferror(fp));
}
/*
* Callback for alias_apply() to print an alias entry.
*/
static int
print_alias_csv(struct sudoers_parse_tree *parse_tree, struct alias *a, void *v
)
{
FILE *fp = v;
const char *title;
debug_decl(print_alias_csv, SUDOERS_DEBUG_UTIL);
title = alias_type_to_string(a->type);
if (title == NULL) {
sudo_warnx("unexpected alias type %d", a->type);
debug_return_int(0);
}
fprintf(fp, "%s,%s,", title, a->name);
print_member_list_csv(fp, parse_tree, &a->members, false, a->type, false);
putc('\n', fp);
debug_return_int(0);
}
/*
* Print all aliases in CSV format:
*/
static bool
print_aliases_csv(FILE *fp, struct sudoers_parse_tree *parse_tree)
{
debug_decl(print_aliases_csv, SUDOERS_DEBUG_UTIL);
if (TAILQ_EMPTY(&parse_tree->defaults))
debug_return_bool(true);
/* Heading line. */
fputs("alias_type,alias_name,members\n", fp);
alias_apply(parse_tree, print_alias_csv, fp);
putc('\n', fp);
debug_return_bool(true);
}
/*
* Print a Cmnd_Spec in CSV format.
*/
static void
print_cmndspec_csv(FILE *fp, struct sudoers_parse_tree *parse_tree,
struct cmndspec *cs, struct cmndspec **nextp,
struct defaults_list *options, bool expand_aliases)
{
char timebuf[sizeof("20120727121554Z")];
struct cmndspec *next = *nextp;
bool need_comma = false;
struct member *m;
struct tm gmt;
bool last_one, quoted = false;
int len;
debug_decl(print_cmndspec_csv, SUDOERS_DEBUG_UTIL);
if (cs->runasuserlist != NULL) {
print_member_list_csv(fp, parse_tree, cs->runasuserlist, false,
RUNASALIAS, expand_aliases);
}
putc(',', fp);
if (cs->runasgrouplist != NULL) {
print_member_list_csv(fp, parse_tree, cs->runasgrouplist, false,
RUNASALIAS, expand_aliases);
}
putc(',', fp);
/* We don't know how many options there will be so always quote it. */
putc('"', fp);
if (cs->notbefore != UNSPEC) {
if (gmtime_r(&cs->notbefore, &gmt) == NULL) {
sudo_warn("%s", U_("unable to get GMT time"));
} else {
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') {
sudo_warnx("%s", U_("unable to format timestamp"));
} else {
fprintf(fp, "%snotbefore=%s", need_comma ? "," : "", timebuf);
need_comma = true;
}
}
}
if (cs->notafter != UNSPEC) {
if (gmtime_r(&cs->notafter, &gmt) == NULL) {
sudo_warn("%s", U_("unable to get GMT time"));
} else {
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') {
sudo_warnx("%s", U_("unable to format timestamp"));
} else {
fprintf(fp, "%snotafter=%s", need_comma ? "," : "", timebuf);
need_comma = true;
}
}
}
if (cs->timeout > 0) {
fprintf(fp, "%scommand_timeout=%d", need_comma ? "," : "", cs->timeout);
need_comma = true;
}
/* Print tags as options */
if (TAGS_SET(cs->tags)) {
struct cmndtag tag = cs->tags;
if (tag.nopasswd != UNSPEC) {
fprintf(fp, "%s%s", need_comma ? "," : "",
tag.nopasswd ? "!authenticate" : "authenticate");
need_comma = true;
}
if (tag.noexec != UNSPEC) {
fprintf(fp, "%s%s", need_comma ? "," : "",
tag.noexec ? "noexec" : "!noexec");
need_comma = true;
}
if (tag.intercept != UNSPEC) {
fprintf(fp, "%s%s", need_comma ? "," : "",
tag.intercept ? "intercept" : "!intercept");
need_comma = true;
}
if (tag.send_mail != UNSPEC) {
if (tag.send_mail) {
fprintf(fp, "%smail_all_cmnds", need_comma ? "," : "");
} else {
fprintf(fp, "%s!mail_all_cmnds,!mail_always,!mail_no_perms",
need_comma ? "," : "");
}
need_comma = true;
}
if (tag.setenv != UNSPEC && tag.setenv != IMPLIED) {
fprintf(fp, "%s%s", need_comma ? "," : "",
tag.setenv ? "setenv" : "!setenv");
need_comma = true;
}
if (tag.follow != UNSPEC) {
fprintf(fp, "%s%s", need_comma ? "," : "",
tag.follow ? "sudoedit_follow" : "!sudoedit_follow");
need_comma = true;
}
if (tag.log_input != UNSPEC) {
fprintf(fp, "%s%s", need_comma ? "," : "",
tag.follow ? "log_input" : "!log_input");
need_comma = true;
}
if (tag.log_output != UNSPEC) {
fprintf(fp, "%s%s", need_comma ? "," : "",
tag.follow ? "log_output" : "!log_output");
need_comma = true;
}
}
print_options_csv(fp, options, need_comma);
if (!TAILQ_EMPTY(options))
need_comma = true;
/* Print runchroot and runcwd. */
if (cs->runchroot != NULL) {
fprintf(fp, "%srunchroot=%s", need_comma ? "," : "", cs->runchroot);
need_comma = true;
}
if (cs->runcwd != NULL) {
fprintf(fp, "%sruncwd=%s", need_comma ? "," : "", cs->runcwd);
need_comma = true;
}
#ifdef HAVE_SELINUX
/* Print SELinux role/type */
if (cs->role != NULL && cs->type != NULL) {
fprintf(fp, "%role=%s,type=%s", need_comma ? "," : "",
cs->role, cs->type);
need_comma = true;
}
#endif /* HAVE_SELINUX */
#ifdef HAVE_PRIV_SET
/* Print Solaris privs/limitprivs */
if (cs->privs != NULL || cs->limitprivs != NULL) {
if (cs->privs != NULL) {
fprintf(fp, "%privs=%s", need_comma ? "," : "", cs->privs);
need_comma = true;
}
if (cs->limitprivs != NULL) {
fprintf(fp, "%limitprivs=%s", need_comma ? "," : "", cs->limitprivs);
need_comma = true;
}
}
#endif /* HAVE_PRIV_SET */
putc('"', fp);
putc(',', fp);
/*
* Merge adjacent commands with matching tags, runas, SELinux
* role/type and Solaris priv settings.
*/
for (;;) {
/* Does the next entry differ only in the command itself? */
/* XXX - move into a function that returns bool */
/* XXX - TAG_SET does not account for implied SETENV */
last_one = next == NULL ||
RUNAS_CHANGED(cs, next) || TAGS_CHANGED(cs->tags, next->tags)
#ifdef HAVE_PRIV_SET
|| cs->privs != next->privs || cs->limitprivs != next->limitprivs
#endif /* HAVE_PRIV_SET */
#ifdef HAVE_SELINUX
|| cs->role != next->role || cs->type != next->type
#endif /* HAVE_SELINUX */
|| cs->runchroot != next->runchroot || cs->runcwd != next->runcwd;
if (!quoted && !last_one) {
quoted = true;
putc('"', fp);
}
m = cs->cmnd;
print_member_csv(fp, parse_tree, m->name, m->type, m->negated, quoted,
CMNDALIAS, expand_aliases);
if (last_one)
break;
putc(',', fp);
cs = next;
next = TAILQ_NEXT(cs, entries);
}
if (quoted)
putc('"', fp);
*nextp = next;
debug_return;
}
/*
* Print a single User_Spec.
*/
static bool
print_userspec_csv(FILE *fp, struct sudoers_parse_tree *parse_tree,
struct userspec *us, bool expand_aliases)
{
struct privilege *priv;
struct cmndspec *cs, *next;
debug_decl(print_userspec_csv, SUDOERS_DEBUG_UTIL);
/*
* Each userspec struct may contain multiple privileges for the user.
*/
TAILQ_FOREACH(priv, &us->privileges, entries) {
TAILQ_FOREACH_SAFE(cs, &priv->cmndlist, entries, next) {
fputs("rule,", fp);
print_member_list_csv(fp, parse_tree, &us->users, false,
USERALIAS, expand_aliases);
putc(',', fp);
print_member_list_csv(fp, parse_tree, &priv->hostlist, false,
HOSTALIAS, expand_aliases);
putc(',', fp);
print_cmndspec_csv(fp, parse_tree, cs, &next, &priv->defaults,
expand_aliases);
putc('\n', fp);
}
}
debug_return_bool(!ferror(fp));
}
/*
* Print User_Specs.
*/
static bool
print_userspecs_csv(FILE *fp, struct sudoers_parse_tree *parse_tree,
bool expand_aliases)
{
struct userspec *us;
debug_decl(print_userspecs_csv, SUDOERS_DEBUG_UTIL);
if (TAILQ_EMPTY(&parse_tree->userspecs))
debug_return_bool(true);
/* Heading line. */
fputs("rule,user,host,runusers,rungroups,options,command\n", fp);
TAILQ_FOREACH(us, &parse_tree->userspecs, entries) {
if (!print_userspec_csv(fp, parse_tree, us, expand_aliases))
debug_return_bool(false);
}
debug_return_bool(true);
}
/*
* Export the parsed sudoers file in CSV format.
*/
bool
convert_sudoers_csv(struct sudoers_parse_tree *parse_tree,
const char *output_file, struct cvtsudoers_config *conf)
{
bool ret = true;
FILE *output_fp = stdout;
debug_decl(convert_sudoers_csv, SUDOERS_DEBUG_UTIL);
if (output_file != NULL && strcmp(output_file, "-") != 0) {
if ((output_fp = fopen(output_file, "w")) == NULL)
sudo_fatal(U_("unable to open %s"), output_file);
}
/* Dump Defaults in CSV format. */
if (!ISSET(conf->suppress, SUPPRESS_DEFAULTS))
print_defaults_csv(output_fp, parse_tree, conf->expand_aliases);
/* Dump Aliases in CSV format. */
if (!conf->expand_aliases && !ISSET(conf->suppress, SUPPRESS_ALIASES)) {
print_aliases_csv(output_fp, parse_tree);
}
/* Dump User_Specs in CSV format. */
if (!ISSET(conf->suppress, SUPPRESS_PRIVS))
print_userspecs_csv(output_fp, parse_tree, conf->expand_aliases);
(void)fflush(output_fp);
if (ferror(output_fp))
ret = false;
if (output_fp != stdout)
fclose(output_fp);
debug_return_bool(ret);
}