From fd88aef1bbbd556ecd8f0baf03a7dd25c3da70bc Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 17 Aug 2026 15:12:04 -0500 Subject: [PATCH 1/2] Added missing NULL checks for parameters flagged by static analysis Ticket: CFE-159 Changelog: none --- cf-agent/files_changes.c | 3 +++ cf-agent/verify_files_utils.c | 5 +++++ libpromises/attributes.c | 2 ++ 3 files changed, 10 insertions(+) diff --git a/cf-agent/files_changes.c b/cf-agent/files_changes.c index 40e1cb4dfe..0d293bf179 100644 --- a/cf-agent/files_changes.c +++ b/cf-agent/files_changes.c @@ -677,6 +677,9 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, const Promise *pp, PromiseResult *result) { + assert(attr != NULL); + assert(sb != NULL); + struct stat cmpsb; CF_DB *dbp; diff --git a/cf-agent/verify_files_utils.c b/cf-agent/verify_files_utils.c index c29b315ab4..c78ad54583 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__ diff --git a/libpromises/attributes.c b/libpromises/attributes.c index 298e42d403..906209428b 100644 --- a/libpromises/attributes.c +++ b/libpromises/attributes.c @@ -1138,6 +1138,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; From 42ba7b811ed84a24120da69e4588150171c473a0 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 17 Aug 2026 15:12:05 -0500 Subject: [PATCH 2/2] Added the silence attribute to body changes The 'silence' attribute takes a list of change categories (content, add, remove, owner, group, perms, device, mtime, inode, stats or all) whose alert output is suppressed. A silenced category no longer logs its alert, while the promise outcome classes, the checksum_alerts class and the persistent audit log in file_changes.log are all still recorded, so policy can keep reacting to the change. Failures are never silenced. Ticket: CFE-159 Changelog: Added the `silence` attribute to `body changes`, which suppresses the alert output of selected file-change categories while still setting the change classes and writing the audit log. --- cf-agent/files_changes.c | 123 +++++++++++--- cf-agent/files_changes.h | 18 ++- cf-agent/verify_files_utils.c | 5 +- libpromises/attributes.c | 55 +++++++ libpromises/cf3.defs.h | 34 ++++ libpromises/eval_context.c | 11 +- libpromises/eval_context.h | 7 + libpromises/mod_files.c | 1 + .../10_files/02_maintain/changes_silent.cf | 153 ++++++++++++++++++ .../02_maintain/changes_silent.cf.sub | 141 ++++++++++++++++ 10 files changed, 515 insertions(+), 33 deletions(-) create mode 100644 tests/acceptance/10_files/02_maintain/changes_silent.cf create mode 100644 tests/acceptance/10_files/02_maintain/changes_silent.cf.sub diff --git a/cf-agent/files_changes.c b/cf-agent/files_changes.c index 0d293bf179..7203baa405 100644 --- a/cf-agent/files_changes.c +++ b/cf-agent/files_changes.c @@ -34,6 +34,35 @@ #include #include +bool IsChangeSilenced(const Attributes *attr, FileChangeSilence categories) +{ + assert(attr != NULL); + + /* An empty set is never silenced, so that a caller which found no change + * to attribute still reports whatever it was going to report. */ + return (categories != FILE_CHANGE_SILENCE_NONE) && + ((categories & ~attr->change.silence) == FILE_CHANGE_SILENCE_NONE); +} + +void RecordFileChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, + FileChangeSilence categories, const char *fmt, ...) +{ + assert(attr != NULL); + + if (IsChangeSilenced(attr, categories)) + { + /* Only the message is suppressed; the promise must still look repaired + * to the policy, which is the whole point of silencing it. */ + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); + return; + } + + va_list ap; + va_start(ap, fmt); + VRecordChange(ctx, pp, attr, fmt, ap); + va_end(ap); +} + /* The format of the changes database is as follows: @@ -493,7 +522,7 @@ bool FileChangesCheckAndUpdateHash(EvalContext *ctx, { found = true; different = (memcmp(digest, dbdigest, size) != 0); - if (different) + if (different && !IsChangeSilenced(attr, FILE_CHANGE_SILENCE_CONTENT)) { Log(LOG_LEVEL_INFO, "Hash '%s' for '%s' changed!", HashNameFromId(type), filename); if (pp->comment) @@ -521,9 +550,10 @@ 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)); + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_CONTENT, + "%s %s hash for '%s' (%s)", + action, HashNameFromId(type), filename, + HashPrintSafe(buffer, sizeof(buffer), digest, type, true)); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); WriteHash(dbp, type, filename, digest); @@ -548,9 +578,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 +651,16 @@ 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); + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_REMOVE, "Removal of '%s' recorded", path); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -705,7 +741,11 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, } else { - RecordChange(ctx, pp, attr, "Wrote stat information for '%s' to database", file); + /* Baselining a file we have never seen is not a change in any + * single category, so it stays quiet only if the caller has + * silenced stat reporting as a whole. */ + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_STATS, + "Wrote stat information for '%s' to database", file); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } } @@ -726,10 +766,20 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, return; } + /* The categories this file actually changed in, so that the summary logged + * after the database write below can tell whether any of it is worth + * reporting. */ + FileChangeSilence changed = FILE_CHANGE_SILENCE_NONE; + 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); + changed |= FILE_CHANGE_SILENCE_PERMS; + + 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", @@ -739,7 +789,7 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded permissions changes in '%s'", file); + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_PERMS, "Recorded permissions changes in '%s'", file); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -752,8 +802,12 @@ 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); + changed |= FILE_CHANGE_SILENCE_OWNER; + 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", @@ -764,7 +818,7 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded ownership changes in '%s'", file); + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_OWNER, "Recorded ownership changes in '%s'", file); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -777,8 +831,12 @@ 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); + changed |= FILE_CHANGE_SILENCE_GROUP; + 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", @@ -789,7 +847,7 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded group changes in '%s'", file); + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_GROUP, "Recorded group changes in '%s'", file); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -802,8 +860,12 @@ 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); + changed |= FILE_CHANGE_SILENCE_DEVICE; + 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", @@ -813,7 +875,7 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded device changes in '%s'", file); + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_DEVICE, "Recorded device changes in '%s'", file); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -826,12 +888,17 @@ 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); + changed |= FILE_CHANGE_SILENCE_INODE; + 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) { + changed |= FILE_CHANGE_SILENCE_MTIME; char from[25]; // ctime() string is 26 bytes (incl NUL) char to[25]; // we ignore the newline at the end // Example: "Thu Nov 24 18:22:48 1986\n" @@ -845,7 +912,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", @@ -855,7 +925,7 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, { if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp)) { - RecordChange(ctx, pp, attr, "Recorded mtime changes in '%s'", file); + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_MTIME, "Recorded mtime changes in '%s'", file); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } else @@ -883,7 +953,8 @@ void FileChangesCheckAndUpdateStats(EvalContext *ctx, } else { - RecordChange(ctx, pp, attr, "Wrote stat information changes for '%s' to database", file); + RecordFileChange(ctx, pp, attr, changed, + "Wrote stat information changes for '%s' to database", file); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } } diff --git a/cf-agent/files_changes.h b/cf-agent/files_changes.h index 1e58aad5e1..ccf7dcd369 100644 --- a/cf-agent/files_changes.h +++ b/cf-agent/files_changes.h @@ -27,6 +27,22 @@ #include +/** + * Returns true if *every* one of the given change categories is silenced by + * the 'silence' attribute of the promise's changes body. An empty category + * set is never silenced. + */ +bool IsChangeSilenced(const Attributes *attr, FileChangeSilence categories); + +/** + * Like RecordChange(), but omits the log message when the change belongs + * entirely to silenced categories. The promise outcome classes are set either + * way, so silencing never hides a change from the policy itself. + */ +void RecordFileChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, + FileChangeSilence categories, const char *fmt, ...) + FUNC_ATTR_PRINTF(5, 6); + typedef enum { FILE_STATE_NEW, @@ -44,7 +60,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 c78ad54583..7931ae8754 100644 --- a/cf-agent/verify_files_utils.c +++ b/cf-agent/verify_files_utils.c @@ -3001,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)); } @@ -3725,7 +3725,8 @@ 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); + RecordFileChange(ctx, pp, attr, FILE_CHANGE_SILENCE_CONTENT, + "Recorded integrity changes in '%s'", file); result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE); } else diff --git a/libpromises/attributes.c b/libpromises/attributes.c index 906209428b..610749f17c 100644 --- a/libpromises/attributes.c +++ b/libpromises/attributes.c @@ -891,6 +891,42 @@ ENTERPRISE_FUNC_0ARG_DEFINE_STUB(HashMethod, GetBestFileChangeHashMethod) return HASH_METHOD_SHA512; } +/* Keep in sync with CF_CHANGE_SILENCE_RANGE in cf3.defs.h, which is what + * rejects unknown category names during policy validation. */ +static const struct +{ + const char *name; + FileChangeSilence categories; +} FILE_CHANGE_SILENCE_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 }, + { "stats", FILE_CHANGE_SILENCE_STATS }, + { "all", FILE_CHANGE_SILENCE_ALL }, +}; + +static FileChangeSilence FileChangeSilenceFromString(const char *s) +{ + assert(s != NULL); + + for (size_t i = 0; + i < sizeof(FILE_CHANGE_SILENCE_CATEGORIES) / sizeof(FILE_CHANGE_SILENCE_CATEGORIES[0]); + i++) + { + if (StringEqual(s, FILE_CHANGE_SILENCE_CATEGORIES[i].name)) + { + return FILE_CHANGE_SILENCE_CATEGORIES[i].categories; + } + } + return FILE_CHANGE_SILENCE_NONE; +} + FileChange GetChangeMgtConstraints(const EvalContext *ctx, const Promise *pp) { FileChange c; @@ -952,6 +988,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); diff --git a/libpromises/cf3.defs.h b/libpromises/cf3.defs.h index af05798119..966f3b8c28 100644 --- a/libpromises/cf3.defs.h +++ b/libpromises/cf3.defs.h @@ -592,6 +592,10 @@ typedef enum #define CF_NAKEDLRANGE "@[(][a-zA-Z0-9_$(){}\\[\\].:]+[)]" #define CF_ANYSTRING ".*" +/* Accepted items of the 'silence' list in a changes body. Keep in sync with + * FILE_CHANGE_SILENCE_CATEGORIES in attributes.c. */ +#define CF_CHANGE_SILENCE_RANGE "content|add|remove|owner|group|perms|device|mtime|inode|stats|all" + #define CF_KEYSTRING "^(SHA|MD5)=[0123456789abcdef]*$" @@ -778,6 +782,35 @@ typedef enum FILE_CHANGE_REPORT_ALL } FileChangeReport; +/*************************************************************************/ + +/* Categories of file change whose alert output the 'silence' attribute of a + * changes body can suppress. Used as a bit set, both for what a promise + * silences and for which categories a single change belongs to. */ +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), + + /* The categories reported by FileChangesCheckAndUpdateStats(). */ + FILE_CHANGE_SILENCE_STATS = (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_ALL = (FILE_CHANGE_SILENCE_STATS | FILE_CHANGE_SILENCE_CONTENT | + FILE_CHANGE_SILENCE_ADD | FILE_CHANGE_SILENCE_REMOVE) +} FileChangeSilence; + +/*************************************************************************/ + typedef enum { PACKAGE_ACTION_ADD, @@ -1104,6 +1137,7 @@ typedef struct FileChangeReport report_changes; int report_diffs; int update; + FileChangeSilence silence; } FileChange; /*************************************************************************/ diff --git a/libpromises/eval_context.c b/libpromises/eval_context.c index 4d25864eaf..dc5947748e 100644 --- a/libpromises/eval_context.c +++ b/libpromises/eval_context.c @@ -3327,20 +3327,23 @@ void cfPS(EvalContext *ctx, LogLevel level, PromiseResult status, const Promise } } -void RecordChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, ...) +void VRecordChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, va_list ap) { assert(ctx != NULL); assert(pp != NULL); assert(attr != NULL); LogPromiseContext(ctx, pp); + VLog(LOG_LEVEL_INFO, fmt, ap); + SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); +} +void RecordChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, ...) +{ va_list ap; va_start(ap, fmt); - VLog(LOG_LEVEL_INFO, fmt, ap); + VRecordChange(ctx, pp, attr, fmt, ap); va_end(ap); - - SetPromiseOutcomeClasses(ctx, PROMISE_RESULT_CHANGE, &(attr->classes)); } void RecordNoChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, ...) diff --git a/libpromises/eval_context.h b/libpromises/eval_context.h index 38d79eb9c6..e15378bbfd 100644 --- a/libpromises/eval_context.h +++ b/libpromises/eval_context.h @@ -331,6 +331,13 @@ void cfPS(EvalContext *ctx, LogLevel level, PromiseResult status, const Promise * multiple changes done by the given promise. */ void RecordChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, ...) FUNC_ATTR_PRINTF(4, 5); + +/** + * va_list variant of RecordChange(), for callers that wrap it in their own + * variadic function. + */ +void VRecordChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, va_list ap); + void RecordNoChange(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, ...) FUNC_ATTR_PRINTF(4, 5); void RecordFailure(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, ...) FUNC_ATTR_PRINTF(4, 5); void RecordWarning(EvalContext *ctx, const Promise *pp, const Attributes *attr, const char *fmt, ...) FUNC_ATTR_PRINTF(4, 5); diff --git a/libpromises/mod_files.c b/libpromises/mod_files.c index 4d501dff62..fc93b4b155 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_CHANGE_SILENCE_RANGE, "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..db87d61d5c --- /dev/null +++ b/tests/acceptance/10_files/02_maintain/changes_silent.cf @@ -0,0 +1,153 @@ +####################################################### +# +# Test the 'silence' attribute of a changes body. +# +# Listing a change category in 'silence' must stop its alert from being +# printed, while leaving everything a policy can act on intact: the promise +# outcome classes, the checksum_alerts class, and the persistent audit log +# in state/file_changes.log. +# +# Every silenced expectation is paired with an unsilenced control file, so +# that an agent where 'silence' does nothing at all fails this test instead +# of passing it. CHECKPOINT_DONE proves the sub-policy reached the end, +# since a policy that aborts early also prints no alerts. +# +# The sub-policy is run from a commands promise rather than execresult() so +# that it runs exactly once; execresult() would re-run it on each evaluation +# pass and we would inspect the output of an already converged run. +# +# Only content, perms and stat categories are covered; owner and group +# silencing would need chown/chgrp, which needs root. +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent init +{ + files: + "$(G.testdir)/silent" + depth_search => recurse("inf"), + file_select => all, + delete => tidy; + + "$(test.output)" delete => tidy; +} + +####################################################### +bundle agent test +{ + vars: + "output" string => "$(G.testdir)/changes_silent.out"; + + commands: + "$(sys.cf_agent) -KI -f $(this.promise_filename).sub > $(output) 2>&1" + contain => in_shell; +} + +####################################################### +bundle agent check +{ + vars: + # Alerts that 'silence' must have suppressed. The stat summary for + # quiet_all.txt is here because it is logged after the individual + # categories and is easy to leave unsilenced. + "must_be_absent" + slist => { + "Hash 'sha256' for '.*quiet_content\.txt' changed!", + "Recorded integrity changes in '.*quiet_content\.txt'", + "Hash 'sha256' for '.*quiet_all\.txt' changed!", + "Recorded integrity changes in '.*quiet_all\.txt'", + "Permissions for '.*quiet_all\.txt' changed", + "Recorded permissions changes in '.*quiet_all\.txt'", + "Wrote stat information changes for '.*quiet_all\.txt'", + "CHECKPOINT_quiet_content_not_kept", + "CHECKPOINT_quiet_all_not_kept", + }; + + # The controls: the same alerts unsilenced, so that a build which + # suppresses nothing still fails, plus the outcome classes that + # silencing must leave alone. + "must_be_present" + slist => { + "Hash 'sha256' for '.*loud_content\.txt' changed!", + "Recorded integrity changes in '.*loud_content\.txt'", + "Permissions for '.*loud_stats\.txt' changed", + "Recorded permissions changes in '.*loud_stats\.txt'", + "Wrote stat information changes for '.*loud_stats\.txt'", + "CHECKPOINT_quiet_content_repaired", + "CHECKPOINT_quiet_all_repaired", + "CHECKPOINT_loud_content_repaired", + "CHECKPOINT_loud_stats_repaired", + "CHECKPOINT_checksum_alerts", + "CHECKPOINT_DONE", + }; + + "log" + string => "$(sys.workdir)$(const.dirsep)state$(const.dirsep)file_changes.log"; + + # A silenced change must still reach the persistent audit log. + "n_quiet_content" + int => countlinesmatching( + ".*quiet_content\.txt,C,Content changed.*", "$(log)" + ); + + "n_quiet_all_content" + int => countlinesmatching( + ".*quiet_all\.txt,C,Content changed.*", "$(log)" + ); + + "n_quiet_all_stats" + int => countlinesmatching(".*quiet_all\.txt,S,.*", "$(log)"); + + classes: + "leaked" + expression => isgreaterthan( + countlinesmatching(".*$(must_be_absent).*", "$(test.output)"), "0" + ); + + "missing" + expression => islessthan( + countlinesmatching(".*$(must_be_present).*", "$(test.output)"), "1" + ); + + "audit_ok" + and => { + isgreaterthan("$(n_quiet_content)", "0"), + isgreaterthan("$(n_quiet_all_content)", "0"), + isgreaterthan("$(n_quiet_all_stats)", "0"), + }; + + "ok" and => { "!leaked", "!missing", "audit_ok" }; + + reports: + DEBUG:: + "silence leaked an alert matching '$(must_be_absent)'" + if => isgreaterthan( + countlinesmatching(".*$(must_be_absent).*", "$(test.output)"), "0" + ); + + "expected output matching '$(must_be_present)' is missing" + if => islessthan( + countlinesmatching(".*$(must_be_present).*", "$(test.output)"), "1" + ); + + "Output of $(this.promise_filename).sub is in '$(test.output)'"; + + DEBUG.!audit_ok:: + "a silenced change is missing from the audit log '$(log)' (content $(n_quiet_content)/$(n_quiet_all_content), stats $(n_quiet_all_stats))"; + + ok:: + "$(this.promise_filename) Pass"; + + !ok:: + "$(this.promise_filename) FAIL"; +} + +### PROJECT_ID: core +### CATEGORY_ID: 27 diff --git a/tests/acceptance/10_files/02_maintain/changes_silent.cf.sub b/tests/acceptance/10_files/02_maintain/changes_silent.cf.sub new file mode 100644 index 0000000000..7f41fcbfea --- /dev/null +++ b/tests/acceptance/10_files/02_maintain/changes_silent.cf.sub @@ -0,0 +1,141 @@ +####################################################### +# +# Sub-policy for changes_silent.cf. +# +# Makes one round of file changes with various 'silence' categories in +# effect. Everything this policy prints is what the parent test inspects, +# so the interesting part is which change alerts do and do not appear. +# +# The CHECKPOINT_ reports let the parent tell "silenced correctly" apart +# from "never ran": a policy that dies early prints no alerts either. +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { "prep", "modify", "monitor", "done" }; +} + +####################################################### +body changes silenced(category) +{ + hash => "sha256"; + report_changes => "all"; + update_hashes => "yes"; + silence => { "$(category)" }; +} + +body changes loud +{ + hash => "sha256"; + report_changes => "all"; + update_hashes => "yes"; +} + +bundle edit_line append_marker +{ + insert_lines: + "SILENT_TEST_MARKER"; +} + +####################################################### +bundle agent prep +# @brief Register the files in the changes database, so that the edits made +# by 'modify' are detected as changes and not as first sightings. +{ + vars: + "dir" string => "$(G.testdir)/silent"; + + files: + "$(dir)/." create => "true"; + + "$(dir)/quiet_content.txt" + create => "true", + changes => silenced("content"), + perms => m("644"); + + "$(dir)/loud_content.txt" + create => "true", + changes => loud, + perms => m("644"); + + "$(dir)/quiet_all.txt" + create => "true", + changes => silenced("all"), + perms => m("644"); + + "$(dir)/loud_stats.txt" + create => "true", + changes => loud, + perms => m("644"); +} + +####################################################### +bundle agent modify +# @brief Change the files behind CFEngine's back, as another service would. +{ + files: + "$(prep.dir)/quiet_content.txt" edit_line => append_marker; + "$(prep.dir)/loud_content.txt" edit_line => append_marker; + + # Content *and* permissions, with every category silenced. + "$(prep.dir)/quiet_all.txt" + edit_line => append_marker, + perms => m("600"); + + # Permissions only, nothing silenced: the control for the stat alerts. + "$(prep.dir)/loud_stats.txt" perms => m("600"); +} + +####################################################### +bundle agent monitor +# @brief Detect the changes made above and record the promise outcomes. +{ + files: + "$(prep.dir)/quiet_content.txt" + changes => silenced("content"), + classes => results("namespace", "quiet_content"); + + "$(prep.dir)/loud_content.txt" + changes => loud, + classes => results("namespace", "loud_content"); + + "$(prep.dir)/quiet_all.txt" + changes => silenced("all"), + classes => results("namespace", "quiet_all"); + + "$(prep.dir)/loud_stats.txt" + changes => loud, + classes => results("namespace", "loud_stats"); + + reports: + # Silencing the alert must not change what the policy can react to. + quiet_content_repaired:: + "CHECKPOINT_quiet_content_repaired"; + + quiet_all_repaired:: + "CHECKPOINT_quiet_all_repaired"; + + loud_content_repaired:: + "CHECKPOINT_loud_content_repaired"; + + loud_stats_repaired:: + "CHECKPOINT_loud_stats_repaired"; + + checksum_alerts:: + "CHECKPOINT_checksum_alerts"; + + # ... and must not turn a detected change into a failure. + quiet_content_not_kept:: + "CHECKPOINT_quiet_content_not_kept"; + + quiet_all_not_kept:: + "CHECKPOINT_quiet_all_not_kept"; +} + +####################################################### +bundle agent done +{ + reports: + "CHECKPOINT_DONE"; +}