diff --git a/cf-agent/simulate_mode.c b/cf-agent/simulate_mode.c index b70274a9c13..25e4193b5c0 100644 --- a/cf-agent/simulate_mode.c +++ b/cf-agent/simulate_mode.c @@ -46,8 +46,13 @@ #define DELIM_CHAR '=' -/* Taken from coreutils/lib/stat-macros.h. */ +/* Taken from coreutils/lib/stat-macros.h. Windows has no setuid, setgid or + * sticky bits. */ +#ifndef __MINGW32__ #define CHMOD_MODE_BITS (S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO) +#else +#define CHMOD_MODE_BITS (S_IRWXU | S_IRWXG | S_IRWXO) +#endif static inline void PrintDelimiter() { @@ -418,6 +423,71 @@ static bool RunDiff(const char *path1, const char *path2) return !failure; } +#ifndef __MINGW32__ +static inline void FormatUser(uid_t uid, char *buf, size_t buf_size) +{ + char name[CF_SMALLBUF]; + NDEBUG_UNUSED int ret; + if (GetUserName(uid, name, sizeof(name), LOG_LEVEL_VERBOSE)) + { + ret = snprintf(buf, buf_size, "%ju/%s", (uintmax_t) uid, name); + } + else + { + ret = snprintf(buf, buf_size, "%ju", (uintmax_t) uid); + } + /* Can't truncate: callers pass CF_BUFSIZE, name is at most CF_SMALLBUF. */ + assert((ret >= 0) && ((size_t) ret < buf_size)); +} + +static inline void FormatGroup(gid_t gid, char *buf, size_t buf_size) +{ + char name[CF_SMALLBUF]; + NDEBUG_UNUSED int ret; + if (GetGroupName(gid, name, sizeof(name), LOG_LEVEL_VERBOSE)) + { + ret = snprintf(buf, buf_size, "%ju/%s", (uintmax_t) gid, name); + } + else + { + ret = snprintf(buf, buf_size, "%ju", (uintmax_t) gid); + } + /* Can't truncate: callers pass CF_BUFSIZE, name is at most CF_SMALLBUF. */ + assert((ret >= 0) && ((size_t) ret < buf_size)); +} +#endif /* !__MINGW32__ */ + +/* Report permission and ownership changes too, 'diff' only compares contents. + * Owner and group are only compared on POSIX. */ +static void DiffStatInfo(const char *path, const struct stat *st_orig, const struct stat *st_chrooted) +{ + assert(st_orig != NULL); + assert(st_chrooted != NULL); + + if ((st_orig->st_mode & CHMOD_MODE_BITS) != (st_chrooted->st_mode & CHMOD_MODE_BITS)) + { + printf("'%s' changed permissions from %04o to %04o\n", path, + st_orig->st_mode & CHMOD_MODE_BITS, st_chrooted->st_mode & CHMOD_MODE_BITS); + } + +#ifndef __MINGW32__ + char from[CF_BUFSIZE]; + char to[CF_BUFSIZE]; + if (st_orig->st_uid != st_chrooted->st_uid) + { + FormatUser(st_orig->st_uid, from, sizeof(from)); + FormatUser(st_chrooted->st_uid, to, sizeof(to)); + printf("'%s' changed owner from %s to %s\n", path, from, to); + } + if (st_orig->st_gid != st_chrooted->st_gid) + { + FormatGroup(st_orig->st_gid, from, sizeof(from)); + FormatGroup(st_chrooted->st_gid, to, sizeof(to)); + printf("'%s' changed group from %s to %s\n", path, from, to); + } +#endif /* !__MINGW32__ */ +} + static bool DiffFile(const char *path) { const char *chrooted_path = ToChangesChroot(path); @@ -455,6 +525,7 @@ static bool DiffFile(const char *path) switch (st_chrooted.st_mode & S_IFMT) { case S_IFREG: case S_IFDIR: + DiffStatInfo(path, &st_orig, &st_chrooted); return RunDiff(path, chrooted_path); default: printf("'%s' is a %s\n", path, GetFileTypeDescription(st_chrooted.st_mode)); diff --git a/tests/acceptance/29_simulate_mode/diff_mode.cf.expected b/tests/acceptance/29_simulate_mode/diff_mode.cf.expected index df7344380f2..7db2f11ae13 100644 --- a/tests/acceptance/29_simulate_mode/diff_mode.cf.expected +++ b/tests/acceptance/29_simulate_mode/diff_mode.cf.expected @@ -102,6 +102,7 @@ Only in WORKDIR/tmp/sub-dir/.: sub-file - +y =========================================================================== +'WORKDIR/tmp/perms' changed permissions from 0600 to 0000 =========================================================================== 'WORKDIR/tmp/transformer' no longer exists =========================================================================== diff --git a/tests/acceptance/29_simulate_mode/diff_mode_ownership.cf b/tests/acceptance/29_simulate_mode/diff_mode_ownership.cf new file mode 100644 index 00000000000..2fcde3b7ab2 --- /dev/null +++ b/tests/acceptance/29_simulate_mode/diff_mode_ownership.cf @@ -0,0 +1,63 @@ +body common control +{ + inputs => { "../default.sub.cf", "./normalize_agent_output.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent init +{ + meta: + "test_skip_needs_work" + string => "using_fakeroot", + comment => "chown is faked under fakeroot; the chrooted stat won't reflect real ownership changes"; + + files: + "$(G.testroot)$(const.dirsep)chown-me" + create => "true", + comment => "Created as root:root, will be promised to bin:bin in the sub policy"; +} + +bundle agent test +{ + meta: + "test_soft_fail" + string => "(solaris|aix|hpux|windows)", + meta => { "ENT-6540", "ENT-10254" }; + + "description" -> { "ENT-14694" } + string => "Test that --simulate=diff reports owner and group changes"; + + commands: + "$(sys.cf_agent) -Kf $(this.promise_dirname)$(const.dirsep)diff_mode_ownership.cf.sub --simulate=diff 2>&1 | $(G.grep) 'changed owner\|changed group' > $(this.promise_filename).actual" + contain => in_shell; +} + +bundle agent check +{ + vars: + "actual" string => readfile("$(this.promise_filename).actual", inf); + + classes: + "has_owner_change" + expression => regcmp( + ".*changed owner from 0/root to [0-9]+/bin.*", "$(actual)" + ); + + "has_group_change" + expression => regcmp( + ".*changed group from 0/root to [0-9]+/bin.*", "$(actual)" + ); + + "ok" and => { "has_owner_change", "has_group_change" }; + + reports: + ok:: + "$(this.promise_filename) Pass"; + + !ok:: + "$(this.promise_filename) FAIL"; + + !ok:: + "actual output: $(actual)"; +} diff --git a/tests/acceptance/29_simulate_mode/diff_mode_ownership.cf.sub b/tests/acceptance/29_simulate_mode/diff_mode_ownership.cf.sub new file mode 100644 index 00000000000..9d24bd19a94 --- /dev/null +++ b/tests/acceptance/29_simulate_mode/diff_mode_ownership.cf.sub @@ -0,0 +1,31 @@ +bundle common simulation_ownership_promises +{ + vars: + "inputs" slist => { "$(this.promise_dirname)/../default.sub.cf" }; +} + +body common control +{ + inputs => { "@(simulation_ownership_promises.inputs)" }; +} + +bundle agent init +{ + files: + "$(G.testroot)$(const.dirsep)chown-me" + create => "true"; +} + +bundle agent test +{ + files: + "$(G.testroot)$(const.dirsep)chown-me" + perms => mog("0600", "bin", "bin"); +} + +bundle agent main +{ + methods: + "init"; + "test"; +}