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";
+}