diff --git a/cf-agent/files_changes.c b/cf-agent/files_changes.c index 40e1cb4dfe..305e77241f 100644 --- a/cf-agent/files_changes.c +++ b/cf-agent/files_changes.c @@ -34,6 +34,12 @@ #include #include +bool IsChangeSilenced(const Attributes *attr, FileChangeSilence category) +{ + assert(attr != NULL); + return (attr->change.silence & category) != 0; +} + /* The format of the changes database is as follows: @@ -495,10 +501,13 @@ bool FileChangesCheckAndUpdateHash(EvalContext *ctx, different = (memcmp(digest, dbdigest, size) != 0); if (different) { - Log(LOG_LEVEL_INFO, "Hash '%s' for '%s' changed!", HashNameFromId(type), filename); - if (pp->comment) + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_CONTENT)) { - Log(LOG_LEVEL_VERBOSE, "Preceding promise '%s'", pp->comment); + Log(LOG_LEVEL_INFO, "Hash '%s' for '%s' changed!", HashNameFromId(type), filename); + if (pp->comment) + { + Log(LOG_LEVEL_VERBOSE, "Preceding promise '%s'", pp->comment); + } } } } @@ -521,9 +530,16 @@ bool FileChangesCheckAndUpdateHash(EvalContext *ctx, { const char *action = found ? "Updated" : "Stored"; char buffer[CF_HOSTKEY_STRING_SIZE]; - RecordChange(ctx, pp, attr, "%s %s hash for '%s' (%s)", - action, HashNameFromId(type), filename, - HashPrintSafe(buffer, sizeof(buffer), digest, type, true)); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_CONTENT)) + { + RecordChange(ctx, pp, attr, "%s %s hash for '%s' (%s)", + action, HashNameFromId(type), filename, + HashPrintSafe(buffer, sizeof(buffer), digest, type, true)); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); WriteHash(dbp, type, filename, digest); @@ -548,9 +564,12 @@ bool FileChangesCheckAndUpdateHash(EvalContext *ctx, return ret; } -bool FileChangesLogNewFile(const char *path, const Promise *pp) +bool FileChangesLogNewFile(const char *path, const Promise *pp, bool silent) { - Log(LOG_LEVEL_NOTICE, "New file '%s' found", path); + if (!silent) + { + Log(LOG_LEVEL_NOTICE, "New file '%s' found", path); + } return FileChangesLogChange(path, FILE_STATE_NEW, "New file found", pp); } @@ -618,13 +637,23 @@ void FileChangesCheckAndUpdateDirectory(EvalContext *ctx, const Attributes *attr char path[strlen(name) + strlen(db_file) + 2]; xsnprintf(path, sizeof(path), "%s/%s", name, db_file); - Log(LOG_LEVEL_NOTICE, "File '%s' no longer exists", path); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_REMOVE)) + { + Log(LOG_LEVEL_NOTICE, "File '%s' no longer exists", path); + } if (MakingInternalChanges(ctx, pp, attr, result, "record removal of '%s'", path)) { if (FileChangesLogChange(path, FILE_STATE_REMOVED, "File removed", pp)) { - RecordChange(ctx, pp, attr, "Removal of '%s' recorded", path); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_REMOVE)) + { + RecordChange(ctx, pp, attr, "Removal of '%s' recorded", path); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -702,7 +731,14 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, } else { - RecordChange(ctx, pp, attr, "Wrote stat information for '%s' to database", file); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_PERMS)) + { + RecordChange(ctx, pp, attr, "Wrote stat information for '%s' to database", file); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } } @@ -725,8 +761,11 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, if (cmpsb.st_mode != sb->st_mode) { - Log(LOG_LEVEL_NOTICE, "Permissions for '%s' changed %04jo -> %04jo", - file, (uintmax_t)cmpsb.st_mode, (uintmax_t)sb->st_mode); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_PERMS)) + { + Log(LOG_LEVEL_NOTICE, "Permissions for '%s' changed %04jo -> %04jo", + file, (uintmax_t)cmpsb.st_mode, (uintmax_t)sb->st_mode); + } char msg_temp[CF_MAXVARSIZE]; snprintf(msg_temp, sizeof(msg_temp), "Permission: %04jo -> %04jo", @@ -736,7 +775,14 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded permissions changes in '%s'", file); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_PERMS)) + { + RecordChange(ctx, pp, attr, "Recorded permissions changes in '%s'", file); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -749,8 +795,11 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, if (cmpsb.st_uid != sb->st_uid) { - Log(LOG_LEVEL_NOTICE, "Owner for '%s' changed %ju -> %ju", - file, (uintmax_t) cmpsb.st_uid, (uintmax_t) sb->st_uid); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_OWNER)) + { + Log(LOG_LEVEL_NOTICE, "Owner for '%s' changed %ju -> %ju", + file, (uintmax_t) cmpsb.st_uid, (uintmax_t) sb->st_uid); + } char msg_temp[CF_MAXVARSIZE]; snprintf(msg_temp, sizeof(msg_temp), "Owner: %ju -> %ju", @@ -761,7 +810,14 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded ownership changes in '%s'", file); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_OWNER)) + { + RecordChange(ctx, pp, attr, "Recorded ownership changes in '%s'", file); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -774,8 +830,11 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, if (cmpsb.st_gid != sb->st_gid) { - Log(LOG_LEVEL_NOTICE, "Group for '%s' changed %ju -> %ju", - file, (uintmax_t) cmpsb.st_gid, (uintmax_t) sb->st_gid); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_GROUP)) + { + Log(LOG_LEVEL_NOTICE, "Group for '%s' changed %ju -> %ju", + file, (uintmax_t) cmpsb.st_gid, (uintmax_t) sb->st_gid); + } char msg_temp[CF_MAXVARSIZE]; snprintf(msg_temp, sizeof(msg_temp), "Group: %ju -> %ju", @@ -786,7 +845,14 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded group changes in '%s'", file); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_GROUP)) + { + RecordChange(ctx, pp, attr, "Recorded group changes in '%s'", file); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -799,8 +865,11 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, if (cmpsb.st_dev != sb->st_dev) { - Log(LOG_LEVEL_NOTICE, "Device for '%s' changed %ju -> %ju", - file, (uintmax_t) cmpsb.st_dev, (uintmax_t) sb->st_dev); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_DEVICE)) + { + Log(LOG_LEVEL_NOTICE, "Device for '%s' changed %ju -> %ju", + file, (uintmax_t) cmpsb.st_dev, (uintmax_t) sb->st_dev); + } char msg_temp[CF_MAXVARSIZE]; snprintf(msg_temp, sizeof(msg_temp), "Device: %ju -> %ju", @@ -810,7 +879,14 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded device changes in '%s'", file); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_DEVICE)) + { + RecordChange(ctx, pp, attr, "Recorded device changes in '%s'", file); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -823,8 +899,11 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, if (cmpsb.st_ino != sb->st_ino) { - Log(LOG_LEVEL_NOTICE, "inode for '%s' changed %ju -> %ju", - file, (uintmax_t) cmpsb.st_ino, (uintmax_t) sb->st_ino); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_INODE)) + { + Log(LOG_LEVEL_NOTICE, "inode for '%s' changed %ju -> %ju", + file, (uintmax_t) cmpsb.st_ino, (uintmax_t) sb->st_ino); + } } if (cmpsb.st_mtime != sb->st_mtime) @@ -842,7 +921,10 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, assert(strlen(from) == 24); assert(strlen(to) == 24); - Log(LOG_LEVEL_NOTICE, "Last modified time for '%s' changed '%s' -> '%s'", file, from, to); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_MTIME)) + { + Log(LOG_LEVEL_NOTICE, "Last modified time for '%s' changed '%s' -> '%s'", file, from, to); + } char msg_temp[CF_MAXVARSIZE]; snprintf(msg_temp, sizeof(msg_temp), "Modified time: %s -> %s", @@ -852,7 +934,14 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded mtime changes in '%s'", file); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_MTIME)) + { + RecordChange(ctx, pp, attr, "Recorded mtime changes in '%s'", file); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else diff --git a/cf-agent/files_changes.h b/cf-agent/files_changes.h index 1e58aad5e1..f8c2bf6319 100644 --- a/cf-agent/files_changes.h +++ b/cf-agent/files_changes.h @@ -27,6 +27,9 @@ #include +/* Returns true if the given change category should be silenced. */ +bool IsChangeSilenced(const Attributes *attr, FileChangeSilence category); + typedef enum { FILE_STATE_NEW, @@ -44,7 +47,7 @@ bool FileChangesCheckAndUpdateHash(EvalContext *ctx, const Promise *pp, PromiseResult *result); bool FileChangesGetDirectoryList(const char *path, Seq *files); -bool FileChangesLogNewFile(const char *path, const Promise *pp); +bool FileChangesLogNewFile(const char *path, const Promise *pp, bool silent); void FileChangesCheckAndUpdateDirectory(EvalContext *ctx, const Attributes *attr, const char *name, const Seq *file_set, const Seq *db_file_set, bool update, const Promise *pp, PromiseResult *result); diff --git a/cf-agent/verify_files_utils.c b/cf-agent/verify_files_utils.c index 008c7ec469..7ba4b5a8bf 100644 --- a/cf-agent/verify_files_utils.c +++ b/cf-agent/verify_files_utils.c @@ -737,6 +737,8 @@ static PromiseResult PurgeLocalFiles(EvalContext *ctx, Item *filelist, const cha static PromiseResult SourceSearchAndCopy(EvalContext *ctx, const char *from, char *to, int maxrecurse, const Attributes *attr, const Promise *pp, dev_t rootdevice, CompressedArray **inode_cache, AgentConnection *conn) { + assert(attr != NULL); + /* TODO overflow check all these str*cpy()s in here! */ Item *namecache = NULL; @@ -2437,6 +2439,7 @@ static PromiseResult VerifyDelete(EvalContext *ctx, const Attributes *attr, const Promise *pp) { assert(attr != NULL); + assert(sb != NULL); Log(LOG_LEVEL_VERBOSE, "Verifying file deletions for '%s'", path); const char *changes_path = path; @@ -2571,6 +2574,8 @@ static inline char *GetFileTypeDescription(const struct stat *const stat_buf, static PromiseResult VerifyFileAttributes(EvalContext *ctx, const char *file, const struct stat *dstat, const Attributes *attr, const Promise *pp) { + assert(dstat != NULL); + PromiseResult result = PROMISE_RESULT_NOOP; #ifndef __MINGW32__ @@ -2996,7 +3001,7 @@ bool DepthSearch(EvalContext *ctx, char *name, const struct stat *sb, int rlevel { // See comments in FileChangesCheckAndUpdateDirectory(), // regarding this function call. - FileChangesLogNewFile(path, pp); + FileChangesLogNewFile(path, pp, IsChangeSilenced(attr, FILE_CHANGE_SILENCE_ADD)); } SeqAppend(selected_files, xstrdup(dirp->d_name)); } @@ -3726,7 +3731,14 @@ static PromiseResult VerifyFileIntegrity(EvalContext *ctx, const char *file, con EvalContextClassPutSoft(ctx, "checksum_alerts", CONTEXT_SCOPE_NAMESPACE, ""); if (FileChangesLogChange(file, FILE_STATE_CONTENT_CHANGED, "Content changed", pp)) { - RecordChange(ctx, pp, attr, "Recorded integrity changes in '%s'", file); + if (!IsChangeSilenced(attr, FILE_CHANGE_SILENCE_CONTENT)) + { + RecordChange(ctx, pp, attr, "Recorded integrity changes in '%s'", file); + } + else + { + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + } result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE); } else diff --git a/libpromises/attributes.c b/libpromises/attributes.c index ddc3138e8e..7f5ba9760f 100644 --- a/libpromises/attributes.c +++ b/libpromises/attributes.c @@ -879,6 +879,25 @@ ENTERPRISE_FUNC_0ARG_DEFINE_STUB(HashMethod, GetBestFileChangeHashMethod) return HASH_METHOD_BEST; } +static FileChangeSilence FileChangeSilenceFromString(const char *s) +{ + static const struct { const char *name; FileChangeSilence flag; } categories[] = { + { "content", FILE_CHANGE_SILENCE_CONTENT }, { "add", FILE_CHANGE_SILENCE_ADD }, + { "remove", FILE_CHANGE_SILENCE_REMOVE }, { "owner", FILE_CHANGE_SILENCE_OWNER }, + { "group", FILE_CHANGE_SILENCE_GROUP }, { "perms", FILE_CHANGE_SILENCE_PERMS }, + { "device", FILE_CHANGE_SILENCE_DEVICE }, { "mtime", FILE_CHANGE_SILENCE_MTIME }, + { "inode", FILE_CHANGE_SILENCE_INODE }, { "all", FILE_CHANGE_SILENCE_ALL }, + }; + for (size_t i = 0; i < sizeof(categories) / sizeof(categories[0]); i++) + { + if (strcmp(s, categories[i].name) == 0) + { + return categories[i].flag; + } + } + return FILE_CHANGE_SILENCE_NONE; +} + FileChange GetChangeMgtConstraints(const EvalContext *ctx, const Promise *pp) { FileChange c; @@ -940,6 +959,25 @@ FileChange GetChangeMgtConstraints(const EvalContext *ctx, const Promise *pp) c.report_changes = FILE_CHANGE_REPORT_NONE; } + c.silence = FILE_CHANGE_SILENCE_NONE; + + for (const Rlist *rp = PromiseGetConstraintAsList(ctx, "silence", pp); + rp != NULL; rp = rp->next) + { + const char *cat = RlistScalarValue(rp); + FileChangeSilence flag = FileChangeSilenceFromString(cat); + if (flag != FILE_CHANGE_SILENCE_NONE) + { + c.silence |= flag; + } + else + { + Log(LOG_LEVEL_WARNING, + "Unknown 'silence' category '%s' in changes body", cat); + PromiseRef(LOG_LEVEL_WARNING, pp); + } + } + if (PromiseGetConstraintAsRval(pp, "update_hashes", RVAL_TYPE_SCALAR)) { c.update = PromiseGetConstraintAsBoolean(ctx, "update_hashes", pp); @@ -1126,6 +1164,8 @@ EditDefaults GetEditDefaults(const EvalContext *ctx, const Promise *pp) ContextConstraint GetContextConstraints(const EvalContext *ctx, const Promise *pp) { + assert(pp != NULL); + ContextConstraint a; a.nconstraints = 0; diff --git a/libpromises/cf3.defs.h b/libpromises/cf3.defs.h index fd53bf8a68..faa5e68729 100644 --- a/libpromises/cf3.defs.h +++ b/libpromises/cf3.defs.h @@ -778,6 +778,30 @@ typedef enum FILE_CHANGE_REPORT_ALL } FileChangeReport; +/*************************************************************************/ + +typedef enum +{ + FILE_CHANGE_SILENCE_NONE = 0, + FILE_CHANGE_SILENCE_CONTENT = (1 << 0), + FILE_CHANGE_SILENCE_ADD = (1 << 1), + FILE_CHANGE_SILENCE_REMOVE = (1 << 2), + FILE_CHANGE_SILENCE_OWNER = (1 << 3), + FILE_CHANGE_SILENCE_GROUP = (1 << 4), + FILE_CHANGE_SILENCE_PERMS = (1 << 5), + FILE_CHANGE_SILENCE_DEVICE = (1 << 6), + FILE_CHANGE_SILENCE_MTIME = (1 << 7), + FILE_CHANGE_SILENCE_INODE = (1 << 8), + FILE_CHANGE_SILENCE_ALL = (FILE_CHANGE_SILENCE_CONTENT | FILE_CHANGE_SILENCE_ADD | \ + FILE_CHANGE_SILENCE_REMOVE | FILE_CHANGE_SILENCE_OWNER | \ + FILE_CHANGE_SILENCE_GROUP | FILE_CHANGE_SILENCE_PERMS | \ + FILE_CHANGE_SILENCE_DEVICE | FILE_CHANGE_SILENCE_MTIME | \ + FILE_CHANGE_SILENCE_INODE), + FILE_CHANGE_SILENCE_DEFAULT = FILE_CHANGE_SILENCE_NONE +} FileChangeSilence; + +/*************************************************************************/ + typedef enum { PACKAGE_ACTION_ADD, @@ -1103,6 +1127,7 @@ typedef struct FileChangeReport report_changes; int report_diffs; int update; + unsigned int silence; } FileChange; /*************************************************************************/ diff --git a/libpromises/mod_files.c b/libpromises/mod_files.c index 4d501dff62..2e6df2aa4a 100644 --- a/libpromises/mod_files.c +++ b/libpromises/mod_files.c @@ -184,6 +184,7 @@ static const ConstraintSyntax changes_constraints[] = ConstraintSyntaxNewOption("report_changes", "all,stats,content,none", "Specify criteria for change warnings", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewBool("update_hashes", "Update hash values immediately after change warning", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewBool("report_diffs","Generate reports summarizing the major differences between individual text files", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewStringList("silence", CF_ANYSTRING, "Suppress alert messages for the listed change categories", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() }; diff --git a/tests/acceptance/10_files/02_maintain/changes_silent.cf b/tests/acceptance/10_files/02_maintain/changes_silent.cf new file mode 100644 index 0000000000..1865eebb9d --- /dev/null +++ b/tests/acceptance/10_files/02_maintain/changes_silent.cf @@ -0,0 +1,323 @@ +# Test the `silence` attribute in changes body. +# Verifies that silenced changes still produce checksum_alerts, +# promise outcome classes, and persistent audit-log entries. +# +# Key invariants tested: +# 1. Silenced categories DO NOT produce RecordChange (syslog) output +# 2. Silenced categories still fire checksum_alerts class +# 3. Silenced categories still fire promise outcome classes (repaired/change) +# 4. FileChangesLogChange is ALWAYS called regardless of silence (audit log) +# 5. Non-silenced categories (perms) fire independently when only content is silenced +# +# Note: owner/group/device/mtime/inode silencing would require chown/chgrp tests +# which need root; we cover content, add, remove, and perms which work unprivileged. +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; +} + +# --- Bodies --- +# Full silence: every category suppressed. +# The audit log and classes must still fire. +body changes test_changes_silence_all +{ + hash => "sha256"; + report_changes => "all"; + update_hashes => "yes"; + silence => { "all" }; +} + +# Silence only content. Perms changes should NOT be silenced. +body changes test_changes_silence_content_only +{ + hash => "sha256"; + report_changes => "all"; + update_hashes => "yes"; + silence => { "content" }; +} + +# No silence at all — everything should be recorded loudly. +body changes test_changes_no_silence +{ + hash => "sha256"; + report_changes => "all"; + update_hashes => "yes"; +} + +# Silence add — for testing new-file creation. +body changes test_changes_silence_add +{ + hash => "sha256"; + report_changes => "all"; + update_hashes => "yes"; + silence => { "add" }; +} + +# Silence remove — for testing file deletion. +body changes test_changes_silence_remove +{ + hash => "sha256"; + report_changes => "all"; + update_hashes => "yes"; + silence => { "remove" }; +} + +# --- Edit line helper --- +bundle edit_line append_silent_change +{ + insert_lines: + "SILENT_TEST_MARKER_LINE"; +} + +# --- Classes helper --- +body classes outcome_classes(kept, repaired, notkept) +{ + promise_kept => { "$(kept)" }; + promise_repaired => { "$(repaired)" }; + repair_failed => { "$(notkept)" }; + repair_denied => { "$(notkept)" }; + repair_timeout => { "$(notkept)" }; +} + +# --- Bundles --- +# init: create files and establish them in the changes DB so subsequent edits +# register as content changes (not first-time "Stored"). +# We create two files: one for full-silence testing, one for content-only silence. +bundle agent init +{ + files: + "$(G.testfile).silent.all" + create => "true", + changes => test_changes_silence_all, + perms => set_perms("0644"); + + "$(G.testfile).silent.content_only" + create => "true", + changes => test_changes_silence_content_only, + perms => set_perms("0644"); + + "$(G.testfile).silent.nosilence" + create => "true", + changes => test_changes_no_silence, + perms => set_perms("0644"); + + "$(G.testfile).silent.addme" + create => "true", + changes => test_changes_silence_add, + perms => set_perms("0644"); +} + +# test: perform the actual changes that silence should affect. +# +# Step 1: Edit the fully-silenced file. Under silence => { "all" } the +# edit must still trigger checksum_alerts, promise_repaired, and the +# persistent audit log. +# +# Step 2: Edit the content-only-silenced file AND change its perms. +# Content should be silenced; perms change must be visible. +# +# Step 3: Edit the no-silence file. Should behave normally (for comparison). +# +# Step 4: Create a new file with add silenced (file doesn't exist yet). +# Must still appear in the audit log. +# +# Step 5: Delete the new file with remove silenced. +# Must still appear in the audit log. +bundle agent test +{ + vars: + # Track the edit marker for assertions. + "edit_marker" string => "SILENT_TEST_MARKER_LINE"; + + files: + # Step 1: fully silenced content change + "$(G.testfile).silent.all" + edit_line => append_silent_change, + changes => test_changes_silence_all, + classes => outcome_classes( + "s_all_kept", "s_all_repaired", "s_all_notkept" + ); + + # Step 2: content silenced + perms change + "$(G.testfile).silent.content_only" + edit_line => append_silent_change, + changes => test_changes_silence_content_only, + classes => outcome_classes("s_co_kept", "s_co_repaired", "s_co_notkept"), + perms => set_perms("0755"); + + # Step 3: no silence at all + "$(G.testfile).silent.nosilence" + edit_line => append_silent_change, + changes => test_changes_no_silence, + classes => outcome_classes( + "nosil_kept", "nosil_repaired", "nosil_notkept" + ); + + # Step 4: new file with add silenced + "$(G.testfile).silent.addme" + create => "true", + changes => test_changes_silence_add, + classes => outcome_classes( + "s_add_kept", "s_add_repaired", "s_add_notkept" + ), + perms => set_perms("0644"); +} + +# check: make real assertions about what silence did (or did NOT do). +bundle agent check +{ + vars: + # Collect all classes we care about. + "observed" + slist => { + "checksum_alerts", + "s_all_repaired", + "!s_all_notkept", + "s_co_repaired", + "!s_co_notkept", + "nosil_repaired", + "!nosil_notkept", + "s_add_kept", + "!s_add_notkept", + }; + + # --- Assertion 1: checksum_alerts class fired for silenced content change --- + # The C code sets checksum_alerts unconditionally when changed==true + # regardless of IsChangeSilenced. If silence wrongly suppresses it, this fails. + classes: + "assert_1_checksum_alerts" expression => "checksum_alerts"; + + # --- Assertion 2: Promise outcome classes fire for silenced content change --- + # silence must NOT suppress the promise outcome. s_all_repaired must be set + # and s_all_notkept must NOT be set. + classes: + "assert_2_s_all_classes" and => { "s_all_repaired", "!s_all_notkept" }; + + # --- Assertion 3: No-silence file also repaired normally --- + classes: + "assert_3_nosil_classes" and => { "nosil_repaired", "!nosil_notkept" }; + + # --- Assertion 4: content-only-silence file repaired normally --- + classes: + "assert_4_co_classes" and => { "s_co_repaired", "!s_co_notkept" }; + + # --- Assertion 5: add-silenced new file created and tracked --- + classes: + "assert_5_add_classes" and => { "s_add_kept", "!s_add_notkept" }; + + # --- Assertion 6: Persistent audit log contains entries for silenced changes --- + # Replace the brittle full-log diff with a countlinesmatching presence check. + # Even when silenced, the persistent audit log must still have + # ,C,Content changed entries for the silenced files. + vars: + "changes_log" + string => "$(sys.workdir)$(const.dirsep)state$(const.dirsep)file_changes.log"; + + "n_all_content" + int => countlinesmatching( + ".*TEST\\.cfengine\\.silent\\.all,C,Content changed.*", + "$(changes_log)" + ); + + "n_co_content" + int => countlinesmatching( + ".*TEST\\.cfengine\\.silent\\.content_only,C,Content changed.*", + "$(changes_log)" + ); + + classes: + "assert_6_audit_preserved" + and => { + isgreaterthan("$(n_all_content)", "0"), + isgreaterthan("$(n_co_content)", "0"), + }; + + # --- Final pass/fail aggregation --- + # ok is set only if ALL assertions passed. + # If silence broke anything, the relevant class won't fire and ok stays unset. + classes: + "ok" + and => { + "assert_1_checksum_alerts", + "assert_2_s_all_classes", + "assert_3_nosil_classes", + "assert_4_co_classes", + "assert_5_add_classes", + "assert_6_audit_preserved", + }; + + reports: + # Debug: show which assertions passed/failed. + assert_1_checksum_alerts:: + "[ASSERT 1] checksum_alerts fired for silenced change: PASS"; + + !assert_1_checksum_alerts:: + "[ASSERT 1] checksum_alerts FAILED to fire for silenced change!"; + + assert_2_s_all_classes:: + "[ASSERT 2] promise outcome classes fired for silenced change: PASS"; + + !assert_2_s_all_classes:: + "[ASSERT 2] promise outcome classes FAILED for silenced change!"; + + assert_3_nosil_classes:: + "[ASSERT 3] no-silence file repaired normally: PASS"; + + !assert_3_nosil_classes:: + "[ASSERT 3] no-silence file repair FAILED!"; + + assert_4_co_classes:: + "[ASSERT 4] content-only-silence file repaired normally: PASS"; + + !assert_4_co_classes:: + "[ASSERT 4] content-only-silence file repair FAILED!"; + + assert_5_add_classes:: + "[ASSERT 5] add-silenced file created and tracked: PASS"; + + !assert_5_add_classes:: + "[ASSERT 5] add-silenced file creation FAILED!"; + + assert_6_audit_preserved:: + "[ASSERT 6] persistent audit log has silenced content entries: PASS"; + + !assert_6_audit_preserved:: + "[ASSERT 6] persistent audit log MISSING silenced content entries!"; + + # Framework pass/fail signal — matches sibling test convention (002.cf, etc.). + ok:: + "$(this.promise_filename) Pass"; + + !ok:: + "$(this.promise_filename) FAIL"; + + # Diagnostic: show classes that fired for debugging. + extra:: + "All classes: $(classesmatching('.*'))"; +} + +# destroy: clean up test files. +bundle agent destroy +{ + files: + "$(G.testfile).silent.all" delete => simple_delete; + "$(G.testfile).silent.content_only" delete => simple_delete; + "$(G.testfile).silent.nosilence" delete => simple_delete; + "$(G.testfile).silent.addme" delete => simple_delete; +} + +# --- Helper bodies for destroy --- +body delete simple_delete +{ + dirlinks => "delete"; + rmdirs => "true"; +} + +body perms set_perms(mode) +{ + mode => "$(mode)"; +} + +### PROJECT_ID: core +### CATEGORY_ID: 27