From 6ca612183c4d73397626ee24c66aa34ccac59c33 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 19 Aug 2026 16:48:41 +0200 Subject: [PATCH 1/3] Added cf_popen_exact_args cf_popen_exact_args is variation of cf_popen when the exact arguments are known at compile time. This skips the quote escaping of ArgSplitCommand which is needed when argv can be of arbitrary length, such as with user input Signed-off-by: Victor Moene --- libpromises/pipes.h | 2 ++ libpromises/pipes_unix.c | 27 ++++++++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/libpromises/pipes.h b/libpromises/pipes.h index 5961ed0140f..f5756e45b63 100644 --- a/libpromises/pipes.h +++ b/libpromises/pipes.h @@ -53,6 +53,8 @@ FILE *cf_popensetuid(const char *command, const Seq *arglist, const char *type, FILE *cf_popen_sh(const char *command, const char *type); FILE *cf_popen_sh_select(const char *command, const char *type, OutputSelect output_select); FILE *cf_popen_shsetuid(const char *command, const char *type, uid_t uid, gid_t gid, char *chdirv, char *chrootv, int background); +FILE *cf_popen_exact_args_select(const char **argv, const char *type, OutputSelect output_select); +FILE *cf_popen_exact_args(const char **argv, const char *type, bool capture_stderr); int cf_pclose(FILE *pp); void cf_pclose_nowait(FILE *pp); bool PipeToPid(pid_t *pid, FILE *pp); diff --git a/libpromises/pipes_unix.c b/libpromises/pipes_unix.c index a9aece3778f..1e9a1b7ba74 100644 --- a/libpromises/pipes_unix.c +++ b/libpromises/pipes_unix.c @@ -371,18 +371,16 @@ IOData cf_popen_full_duplex(const char *command, bool capture_stderr, bool requi } } -FILE *cf_popen_select(const char *command, const char *type, OutputSelect output_select) +// do not use with user input +FILE *cf_popen_exact_args_select(const char **argv, const char *type, OutputSelect output_select) { int pd[2]; pid_t pid; FILE *pp = NULL; - char **argv = ArgSplitCommand(command, NULL); - pid = CreatePipeAndFork(type, pd); if (pid == (pid_t) -1) { - ArgFree(argv); return NULL; } @@ -427,7 +425,6 @@ FILE *cf_popen_select(const char *command, const char *type, OutputSelect output if ((pp = fdopen(pd[0], type)) == NULL) { cf_pwait(pid); - ArgFree(argv); return NULL; } break; @@ -439,13 +436,11 @@ FILE *cf_popen_select(const char *command, const char *type, OutputSelect output if ((pp = fdopen(pd[1], type)) == NULL) { cf_pwait(pid); - ArgFree(argv); return NULL; } } ChildrenFDSet(fileno(pp), pid); - ArgFree(argv); return pp; } @@ -453,6 +448,24 @@ FILE *cf_popen_select(const char *command, const char *type, OutputSelect output return NULL; } +// do not use with user input +FILE *cf_popen_exact_args(const char **argv, const char *type, bool capture_stderr) +{ + return cf_popen_exact_args_select( + argv, + type, + capture_stderr ? OUTPUT_SELECT_BOTH : OUTPUT_SELECT_STDOUT); +} + +FILE *cf_popen_select(const char *command, const char *type, OutputSelect output_select) +{ + char **argv = ArgSplitCommand(command, NULL); + FILE *ret = cf_popen_exact_args_select(argv, type, output_select); + ArgFree(argv); + + return ret; +} + FILE *cf_popen(const char *command, const char *type, bool capture_stderr) { return cf_popen_select( From 7c1e1dd415e54d978f6e03eb3dd0ca6e11033f84 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 19 Aug 2026 16:52:13 +0200 Subject: [PATCH 2/3] Added strtotime policy function Ticket: CFE-3882 Changeglog: Title Signed-off-by: Victor Moene --- libpromises/evalfunction.c | 103 ++++++++++++++++++ .../01_vars/02_functions/strtotime.cf | 29 +++++ 2 files changed, 132 insertions(+) create mode 100644 tests/acceptance/01_vars/02_functions/strtotime.cf diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index 9662bcd9e80..2c5dee9ba2d 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -7597,6 +7597,101 @@ static FnCallResult FnCallStrftime(ARG_UNUSED EvalContext *ctx, /*********************************************************************/ +static const char *date_paths[] = { + "/usr/bin/date", + "/bin/date", + NULL +}; + +static const char *LocateDateBinary() +{ + for (size_t i = 0; date_paths[i] != NULL; i++) + { + const char *path = date_paths[i]; + + if (IsExecutable(path)) + { + return path; + } + } + return NULL; +} + +static int ParseDate(const char *input_string, time_t *out) +{ + const char *date_path = LocateDateBinary(); + + if (date_path == NULL) + { + Log(LOG_LEVEL_ERR, "Unable to find 'date' binary"); + return -1; + } + + char buffer[MAX_INPUT]; + int n = snprintf(buffer, sizeof(buffer), "--date=%s", input_string); + + if (n < 0 || (size_t) n >= sizeof(buffer)) { + Log(LOG_LEVEL_ERR, "Truncation error: input string '%.10s...' is too long (%d >= %zu)", + input_string, n, sizeof(buffer)); + return -1; + } + + const char *argv[] = {date_path, buffer, "+%s", NULL}; + FILE *fd = cf_popen_exact_args(argv, "r", true); + + if (fd == NULL) + { + Log(LOG_LEVEL_ERR, "Couldn't run date \"--date='%s' +%%s\"", input_string); + return -1; + } + + size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1 , fd); + buffer[bytes_read] = '\0'; + + if (bytes_read == 0) + { + if (ferror(fd)) + { + Log(LOG_LEVEL_ERR, "Error reading output for '%s'", input_string); + } + else if (feof(fd)) + { + Log(LOG_LEVEL_DEBUG, "No output read for '%s'", input_string); + } + fclose(fd); + return -1; + } + fclose(fd); + + int ret = StringToLong(buffer, (long *) out); + if (ret != 0) + { + LogStringToLongError(buffer, "ParseDate", ret); + return -1; + } + + return 0; +} + +static FnCallResult FnCallStrToTime(ARG_UNUSED EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) +{ + assert(fp != NULL); + + const char *input_string = RlistScalarValue(finalargs); + time_t result; + int ret = ParseDate(input_string, &result); + + if (ret != 0) + { + Log(LOG_LEVEL_ERR, "'%s': Invalid date '%s'", fp->name, input_string); + return FnFailure(); + } + + return FnReturnF("%ld", result); +} + +/*********************************************************************/ + static FnCallResult FnCallEval(EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) { if (finalargs == NULL) @@ -11427,6 +11522,12 @@ static const FnCallArg STRFTIME_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; +static const FnCallArg STRTOTIME_ARGS[] = +{ + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "String to parse"}, + {NULL, CF_DATA_TYPE_NONE, NULL} +}; + static const FnCallArg STRING_REPLACE_ARGS[] = { {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Source string"}, @@ -12003,6 +12104,8 @@ const FnCallType CF_FNCALL_TYPES[] = FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("strftime", CF_DATA_TYPE_STRING, STRFTIME_ARGS, &FnCallStrftime, "Format a date and time string", FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), + FnCallTypeNew("strtotime", CF_DATA_TYPE_INT, STRTOTIME_ARGS, &FnCallStrToTime, "Parse a timestamp from a string", + FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("sublist", CF_DATA_TYPE_STRING_LIST, SUBLIST_ARGS, &FnCallSublist, "Returns arg3 element from either the head or the tail (according to arg2) of list or array or data container arg1.", FNCALL_OPTION_COLLECTING, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("sysctlvalue", CF_DATA_TYPE_STRING, SYSCTLVALUE_ARGS, &FnCallSysctlValue, "Returns a value for sysctl key arg1 pair", diff --git a/tests/acceptance/01_vars/02_functions/strtotime.cf b/tests/acceptance/01_vars/02_functions/strtotime.cf new file mode 100644 index 00000000000..1b6d5eb98ac --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/strtotime.cf @@ -0,0 +1,29 @@ +####################################################### +# +# Test strtotime function +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent test +{ + vars: + "some_date" int => strtotime("2009-09-21T00:00:00Z"); + "epoch" int => int(1253491200); + + classes: + "ok" expression => strcmp("$(epoch)", "$(some_date)"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + + !ok:: + "$(this.promise_filename) FAIL"; +} From 25733e62dc431df82490bb4f6940d4d29e0eb1e6 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 19 Aug 2026 17:22:47 +0200 Subject: [PATCH 3/3] Added file_older_than policy function Changelog: Title Signed-off-by: Victor Moene --- libpromises/evalfunction.c | 76 +++++++++++++++++++ .../01_vars/02_functions/file_older_than.cf | 64 ++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 tests/acceptance/01_vars/02_functions/file_older_than.cf diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index 2c5dee9ba2d..ef86d73c4b0 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -7692,6 +7692,71 @@ static FnCallResult FnCallStrToTime(ARG_UNUSED EvalContext *ctx, ARG_UNUSED cons /*********************************************************************/ +static FnCallResult FnCallFileOlderThan(ARG_UNUSED EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) +{ + assert(fp != NULL); + + if (finalargs == NULL) + { + Log(LOG_LEVEL_ERR, "Function '%s' requires path as first argument", + fp->name); + return FnFailure(); + } + const char *filename = RlistScalarValue(finalargs); + + if (finalargs->next == NULL) + { + Log(LOG_LEVEL_ERR, "Function '%s' requires date or date offset as second argument", + fp->name); + return FnFailure(); + } + const char *date = RlistScalarValue(finalargs->next); + const char *option = (finalargs->next->next != NULL) ? RlistScalarValue(finalargs->next->next) : "modification"; + + struct stat statbuf; + + if (stat(filename, &statbuf) != 0) + { + Log(LOG_LEVEL_ERR, "'%s': Couldn't stat '%s'", fp->name, filename); + return FnFailure(); + } + + time_t file_ts; + + if (StringEqual_IgnoreCase(option, "modification") || StringEqual_IgnoreCase(option, "modif") ) + { + file_ts = statbuf.st_mtime; + } + else if (StringEqual_IgnoreCase(option, "access")) + { + file_ts = statbuf.st_atime; + } + else if (StringEqual_IgnoreCase(option, "change")) + { + file_ts = statbuf.st_ctime; + } + else + { + ProgrammingError("Unknown option for %s\n", fp->name); + } + + time_t input_time; + int ret = ParseDate(date, &input_time); + + if (ret != 0) + { + Log(LOG_LEVEL_ERR, "'%s': Couldn't parse '%s'", fp->name, date); + return FnFailure(); + } + + time_t now = time(NULL); + time_t offset = input_time - now; // convert date to an offset + + return FnReturnContext(file_ts + offset <= now); +} + +/*********************************************************************/ + static FnCallResult FnCallEval(EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) { if (finalargs == NULL) @@ -11756,6 +11821,14 @@ static const FnCallArg ISREADABLE_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; +static const FnCallArg FILE_OLDER_THAN_ARGS[] = +{ + {CF_ABSPATHRANGE, CF_DATA_TYPE_STRING, "Path to file"}, + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Date string"}, + {"modification,modif,access,change", CF_DATA_TYPE_OPTION, "file timespec"}, + {NULL, CF_DATA_TYPE_NONE, NULL} +}; + static const FnCallArg DATATYPE_ARGS[] = { {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Variable identifier"}, @@ -12206,6 +12279,9 @@ const FnCallType CF_FNCALL_TYPES[] = FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(2, 3)), FnCallTypeNew("isreadable", CF_DATA_TYPE_CONTEXT, ISREADABLE_ARGS, &FnCallIsReadable, "Check if file is readable. Timeout immediately or after optional timeout interval", FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(1, 2)), + FnCallTypeNew("file_older_than", CF_DATA_TYPE_CONTEXT, FILE_OLDER_THAN_ARGS, &FnCallFileOlderThan, "Check if file is older than a time offset or date.", + FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(2, 3)), + // Datatype functions FnCallTypeNew("type", CF_DATA_TYPE_STRING, DATATYPE_ARGS, &FnCallDatatype, "Get type description as string", diff --git a/tests/acceptance/01_vars/02_functions/file_older_than.cf b/tests/acceptance/01_vars/02_functions/file_older_than.cf new file mode 100644 index 00000000000..4f0a24041ad --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/file_older_than.cf @@ -0,0 +1,64 @@ +####################################################### +# +# Test file_older_than function +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent init +{ + files: + "$(G.testdir)/somefile.txt" + create => "true", + content => "Hello world!"; +} + +####################################################### +bundle agent test +{ + classes: + "long_time_past" + expression => file_older_than( + "$(G.testdir)/somefile.txt", "100 years ago" + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "long_time_future" + expression => not( + file_older_than("$(G.testdir)/somefile.txt", "100 years") + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "newer_than_a_week" + expression => file_older_than( + "$(G.testdir)/somefile.txt", "-1 week", "modif" + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "ok" + expression => and( + "long_time_past", "long_time_future", "newer_than_a_week" + ), + if => fileexists("$(G.testdir)/somefile.txt"), + scope => "namespace"; +} + +####################################################### +bundle agent check +{ + files: + "$(G.testdir)/somefile.txt" delete => tidy; + + reports: + ok:: + "$(this.promise_filename) Pass"; + + !ok:: + "$(this.promise_filename) FAIL"; +}