From 3f0ca3d00c15a69042f8d70e149db141efabb67f Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 12 May 2026 01:02:53 +0530 Subject: [PATCH] Support dpkginfo name pattern operations Enumerate Debian package status entries for dpkginfo_object name matching and filter them through the common OVAL entity comparison helper. This supports equals, not equal, and pattern match operations while preserving exact virtual package lookup through Provides entries. Add an offline dpkginfo probe regression test covering exact, pattern, not equal, and virtual package names. Fixes #2338. --- src/OVAL/probes/unix/linux/dpkginfo-helper.c | 263 +++++++++++++----- src/OVAL/probes/unix/linux/dpkginfo-helper.h | 4 + src/OVAL/probes/unix/linux/dpkginfo_probe.c | 53 +++- tests/probes/CMakeLists.txt | 1 + tests/probes/dpkginfo/CMakeLists.txt | 3 + tests/probes/dpkginfo/test_probes_dpkginfo.sh | 70 +++++ .../probes/dpkginfo/test_probes_dpkginfo.xml | 97 +++++++ 7 files changed, 411 insertions(+), 80 deletions(-) create mode 100644 tests/probes/dpkginfo/CMakeLists.txt create mode 100755 tests/probes/dpkginfo/test_probes_dpkginfo.sh create mode 100644 tests/probes/dpkginfo/test_probes_dpkginfo.xml diff --git a/src/OVAL/probes/unix/linux/dpkginfo-helper.c b/src/OVAL/probes/unix/linux/dpkginfo-helper.c index 34f20b18d2..0829849a8a 100644 --- a/src/OVAL/probes/unix/linux/dpkginfo-helper.c +++ b/src/OVAL/probes/unix/linux/dpkginfo-helper.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "debug_priv.h" #include "dpkginfo-helper.h" @@ -65,14 +66,129 @@ static int version(struct dpkginfo_reply_t *reply) return -1; } -struct dpkginfo_reply_t* dpkginfo_get_by_name(const char *name, int *err) +static void dpkginfo_clear_reply(struct dpkginfo_reply_t *reply) +{ + if (reply == NULL) + return; + free(reply->name); + free(reply->arch); + free(reply->epoch); + free(reply->release); + free(reply->version); + free(reply->evr); + memset(reply, 0, sizeof(*reply)); +} + +static char *strdup_nullable(const char *str) +{ + return str == NULL ? NULL : strdup(str); +} + +static int dpkginfo_copy_reply(struct dpkginfo_reply_t *dst, const struct dpkginfo_reply_t *src, const char *name) +{ + memset(dst, 0, sizeof(*dst)); + dst->name = strdup(name); + dst->arch = strdup_nullable(src->arch); + dst->epoch = strdup_nullable(src->epoch); + dst->release = strdup_nullable(src->release); + dst->version = strdup_nullable(src->version); + dst->evr = strdup_nullable(src->evr); + + if (dst->name == NULL || (src->arch != NULL && dst->arch == NULL) || + (src->epoch != NULL && dst->epoch == NULL) || + (src->release != NULL && dst->release == NULL) || + (src->version != NULL && dst->version == NULL) || + (src->evr != NULL && dst->evr == NULL)) { + dpkginfo_clear_reply(dst); + return -1; + } + + return 0; +} + +static int append_reply(struct dpkginfo_reply_t **replies, size_t *reply_count, const struct dpkginfo_reply_t *reply, const char *name) +{ + void *new_replies = realloc(*replies, (*reply_count + 1) * sizeof(**replies)); + if (new_replies == NULL) + return -1; + + *replies = new_replies; + if (dpkginfo_copy_reply(*replies + *reply_count, reply, name) != 0) + return -1; + + (*reply_count)++; + return 0; +} + +static int append_string(char ***strings, size_t *string_count, const char *str) +{ + void *new_strings = realloc(*strings, (*string_count + 1) * sizeof(**strings)); + if (new_strings == NULL) + return -1; + + *strings = new_strings; + (*strings)[*string_count] = strdup(str); + if ((*strings)[*string_count] == NULL) + return -1; + + (*string_count)++; + return 0; +} + +static void free_strings(char **strings, size_t string_count) +{ + for (size_t i = 0; i < string_count; i++) + free(strings[i]); + free(strings); +} + +static int finish_package(const struct dpkginfo_reply_t *package, bool not_installed, + char **provides, size_t provides_count, struct dpkginfo_reply_t **replies, size_t *reply_count) +{ + if (package->name == NULL || not_installed) + return 0; + + if (append_reply(replies, reply_count, package, package->name) != 0) + return -1; + + for (size_t i = 0; i < provides_count; i++) { + if (append_reply(replies, reply_count, package, provides[i]) != 0) + return -1; + } + + return 0; +} + +static int add_provides(char *value, char ***provides, size_t *provides_count) +{ + char *state = NULL; + char *provider = strtok_r(value, ",", &state); + + while (provider != NULL) { + provider = trimleft(provider); + char *version_separator = strchr(provider, ' '); + if (version_separator != NULL) + *version_separator = '\0'; + if (*provider != '\0' && append_string(provides, provides_count, provider) != 0) + return -1; + provider = strtok_r(NULL, ",", &state); + } + + return 0; +} + +struct dpkginfo_reply_t *dpkginfo_get_all(size_t *reply_count, int *err) { FILE *f; - char buf[DPKG_STATUS_BUFFER_SIZE], path[PATH_MAX], *root, *key, *value, *p; - struct dpkginfo_reply_t *reply; + char buf[DPKG_STATUS_BUFFER_SIZE], path[PATH_MAX], *root, *key, *value; + struct dpkginfo_reply_t package = { 0 }; + struct dpkginfo_reply_t *replies = NULL; + char **provides = NULL; + size_t provides_count = 0; + bool not_installed = false; *err = 0; - reply = NULL; + *reply_count = 0; root = getenv("OSCAP_PROBE_ROOT"); if (root != NULL) @@ -87,21 +203,16 @@ struct dpkginfo_reply_t* dpkginfo_get_by_name(const char *name, int *err) return NULL; } - dD("Searching package \"%s\".", name); - while (fgets(buf, DPKG_STATUS_BUFFER_SIZE, f)) { if (buf[0] == '\n') { // New package entry. - if (reply != NULL) { - if (reply->name != NULL) { - // Package found. - goto out; - } else { - // Package not found yet. - dpkginfo_free_reply(reply); - reply = NULL; - } - } + if (finish_package(&package, not_installed, provides, provides_count, &replies, reply_count) != 0) + goto err; + dpkginfo_clear_reply(&package); + free_strings(provides, provides_count); + provides = NULL; + provides_count = 0; + not_installed = false; continue; } if (isspace(buf[0])) { @@ -119,71 +230,87 @@ struct dpkginfo_reply_t* dpkginfo_get_by_name(const char *name, int *err) value = trimleft(value); // Package should be the first line. if (strcmp(key, "Package") == 0) { - if (reply != NULL) - continue; - reply = calloc(1, sizeof(*reply)); - if (reply == NULL) + if (finish_package(&package, not_installed, provides, provides_count, &replies, reply_count) != 0) goto err; - if (strcmp(value, name) == 0) { - reply->name = strdup(value); - if (reply->name == NULL) - goto err; - } - } else if (reply != NULL) { + dpkginfo_clear_reply(&package); + free_strings(provides, provides_count); + provides = NULL; + provides_count = 0; + not_installed = false; + + package.name = strdup(value); + if (package.name == NULL) + goto err; + } else if (package.name != NULL) { if (strcmp(key, "Status") == 0) { - if (strncmp(value, "install", 7) != 0) { - // Package deinstalled. - dD("Package \"%s\" has been deinstalled.", name); - dpkginfo_free_reply(reply); - reply = NULL; - continue; - } + not_installed = strncmp(value, "install", 7) != 0; } else if (strcmp(key, "Architecture") == 0) { - reply->arch = strdup(value); - if (reply->arch == NULL) + package.arch = strdup(value); + if (package.arch == NULL) goto err; } else if (strcmp(key, "Version") == 0) { - reply->evr = strdup(value); - if (reply->evr == NULL) + package.evr = strdup(value); + if (package.evr == NULL) goto err; - if (version(reply) < 0) + if (version(&package) < 0) goto err; } else if (strcmp(key, "Provides") == 0) { - // Handle virtual packages. - char *s = strtok(value, ","); - while (s != NULL) { - s = trimleft(s); - // Ignore version. - p = strchr(s, ' '); - if (p != NULL) - *p++ = '\0'; - if (strcmp(s, name) == 0) { - reply->name = strdup(value); - if (reply->name == NULL) - goto err; - break; - } - s = strtok(NULL, ","); - } + if (add_provides(value, &provides, &provides_count) != 0) + goto err; } } } // Reached end of file. + if (finish_package(&package, not_installed, provides, provides_count, &replies, reply_count) != 0) + goto err; -out: - if (reply != NULL) { - // Package found. - dD("Package \"%s\" found (arch=%s evr=%s epoch=%s version=%s release=%s).", - name, reply->arch, reply->evr, reply->epoch, reply->version, reply->release); + if (*reply_count > 0) { + dD("Found %zu dpkg package entries.", *reply_count); *err = 1; } + dpkginfo_clear_reply(&package); + free_strings(provides, provides_count); fclose(f); - return reply; + return replies; err: dW("Insufficient memory available to allocate duplicate string."); fclose(f); - dpkginfo_free_reply(reply); + dpkginfo_clear_reply(&package); + free_strings(provides, provides_count); + dpkginfo_free_replies(replies, *reply_count); + *reply_count = 0; + *err = -1; + return NULL; +} + +struct dpkginfo_reply_t* dpkginfo_get_by_name(const char *name, int *err) +{ + size_t reply_count = 0; + struct dpkginfo_reply_t *replies = dpkginfo_get_all(&reply_count, err); + + if (replies == NULL) + return NULL; + + for (size_t i = 0; i < reply_count; i++) { + if (strcmp(replies[i].name, name) == 0) { + struct dpkginfo_reply_t *reply = malloc(sizeof(*reply)); + if (reply == NULL) + goto err; + *reply = replies[i]; + memset(replies + i, 0, sizeof(replies[i])); + dpkginfo_free_replies(replies, reply_count); + *err = 1; + return reply; + } + } + + dpkginfo_free_replies(replies, reply_count); + *err = 0; + return NULL; + +err: + dpkginfo_free_replies(replies, reply_count); *err = -1; return NULL; } @@ -191,12 +318,14 @@ struct dpkginfo_reply_t* dpkginfo_get_by_name(const char *name, int *err) void dpkginfo_free_reply(struct dpkginfo_reply_t *reply) { if (reply) { - free(reply->name); - free(reply->arch); - free(reply->epoch); - free(reply->release); - free(reply->version); - free(reply->evr); + dpkginfo_clear_reply(reply); free(reply); } } + +void dpkginfo_free_replies(struct dpkginfo_reply_t *replies, size_t reply_count) +{ + for (size_t i = 0; i < reply_count; i++) + dpkginfo_clear_reply(replies + i); + free(replies); +} diff --git a/src/OVAL/probes/unix/linux/dpkginfo-helper.h b/src/OVAL/probes/unix/linux/dpkginfo-helper.h index 72f5934252..61bb3ce443 100644 --- a/src/OVAL/probes/unix/linux/dpkginfo-helper.h +++ b/src/OVAL/probes/unix/linux/dpkginfo-helper.h @@ -22,6 +22,8 @@ #ifndef __DPKGINFO_HELPER__ #define __DPKGINFO_HELPER__ +#include + struct dpkginfo_reply_t { char *name; char *arch; @@ -32,7 +34,9 @@ struct dpkginfo_reply_t { }; struct dpkginfo_reply_t * dpkginfo_get_by_name(const char *name, int *err); +struct dpkginfo_reply_t *dpkginfo_get_all(size_t *reply_count, int *err); void dpkginfo_free_reply(struct dpkginfo_reply_t *reply); +void dpkginfo_free_replies(struct dpkginfo_reply_t *replies, size_t reply_count); #endif /* __DPKGINFO_HELPER__ */ diff --git a/src/OVAL/probes/unix/linux/dpkginfo_probe.c b/src/OVAL/probes/unix/linux/dpkginfo_probe.c index 319004ea7f..ed1fc61b5a 100644 --- a/src/OVAL/probes/unix/linux/dpkginfo_probe.c +++ b/src/OVAL/probes/unix/linux/dpkginfo_probe.c @@ -58,6 +58,7 @@ #include "public/oval_schema_version.h" #include +#include "probe/entcmp.h" #include "dpkginfo-helper.h" @@ -71,8 +72,10 @@ int dpkginfo_probe_main (probe_ctx *ctx, void *arg) { SEXP_t *val, *item, *ent, *obj; char *request_st = NULL; - struct dpkginfo_reply_t *dpkginfo_reply = NULL; + struct dpkginfo_reply_t *dpkginfo_replies = NULL; + size_t dpkginfo_reply_count = 0; int errflag; + oval_operation_t operation; obj = probe_ctx_getobject(ctx); ent = probe_obj_getent(obj, "name", 1); @@ -110,10 +113,29 @@ int dpkginfo_probe_main (probe_ctx *ctx, void *arg) } } - /* get info from debian apt cache */ - dpkginfo_reply = dpkginfo_get_by_name(request_st, &errflag); - - if (dpkginfo_reply == NULL) { + val = probe_ent_getattrval(ent, "operation"); + if (val == NULL) { + operation = OVAL_OPERATION_EQUALS; + } else { + operation = (oval_operation_t) SEXP_number_geti_32(val); + SEXP_free(val); + } + + switch (operation) { + case OVAL_OPERATION_EQUALS: + case OVAL_OPERATION_NOT_EQUAL: + case OVAL_OPERATION_PATTERN_MATCH: + break; + default: + SEXP_free(ent); + free(request_st); + return PROBE_EOPNOTSUPP; + } + + /* get info from Debian package status */ + dpkginfo_replies = dpkginfo_get_all(&dpkginfo_reply_count, &errflag); + + if (dpkginfo_replies == NULL) { switch (errflag) { case 0: /* Not found */ { @@ -122,7 +144,7 @@ int dpkginfo_probe_main (probe_ctx *ctx, void *arg) } case -1: /* Error */ { - dD("dpkginfo_get_by_name failed."); + dD("dpkginfo_get_all failed."); item = probe_item_create(OVAL_LINUX_DPKG_INFO, NULL, "name", OVAL_DATATYPE_STRING, request_st, NULL); @@ -130,10 +152,8 @@ int dpkginfo_probe_main (probe_ctx *ctx, void *arg) probe_item_collect(ctx, item); break; } - } + } } else { /* Ok */ - int i; - int num_items = 1; /* FIXME */ oval_datatype_t evr_string_type; oval_schema_version_t oval_version = probe_obj_get_platform_schema_version(obj); if (oval_schema_version_cmp(oval_version, OVAL_SCHEMA_VERSION(5.11.1)) >= 0) { @@ -142,10 +162,18 @@ int dpkginfo_probe_main (probe_ctx *ctx, void *arg) evr_string_type = OVAL_DATATYPE_EVR_STRING; } - for (i = 0; i < num_items; ++i) { + for (size_t i = 0; i < dpkginfo_reply_count; ++i) { + SEXP_t *name = SEXP_string_newf("%s", dpkginfo_replies[i].name); + + if (probe_entobj_cmp(ent, name) != OVAL_RESULT_TRUE) { + SEXP_free(name); + continue; + } + + struct dpkginfo_reply_t *dpkginfo_reply = dpkginfo_replies + i; dD("%s: element found version %s", dpkginfo_reply->name, dpkginfo_reply->evr); item = probe_item_create (OVAL_LINUX_DPKG_INFO, NULL, - "name", OVAL_DATATYPE_STRING, dpkginfo_reply->name, + "name", OVAL_DATATYPE_SEXP, name, "arch", OVAL_DATATYPE_STRING, dpkginfo_reply->arch, "epoch", OVAL_DATATYPE_STRING, dpkginfo_reply->epoch, "release", OVAL_DATATYPE_STRING, dpkginfo_reply->release, @@ -154,9 +182,8 @@ int dpkginfo_probe_main (probe_ctx *ctx, void *arg) NULL); probe_item_collect(ctx, item); - - dpkginfo_free_reply(dpkginfo_reply); } + dpkginfo_free_replies(dpkginfo_replies, dpkginfo_reply_count); } SEXP_free(ent); diff --git a/tests/probes/CMakeLists.txt b/tests/probes/CMakeLists.txt index 7dd3efd658..92d979b672 100644 --- a/tests/probes/CMakeLists.txt +++ b/tests/probes/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory("dpkginfo") add_subdirectory("environmentvariable") add_subdirectory("environmentvariable58") add_subdirectory("family") diff --git a/tests/probes/dpkginfo/CMakeLists.txt b/tests/probes/dpkginfo/CMakeLists.txt new file mode 100644 index 0000000000..f7b9853491 --- /dev/null +++ b/tests/probes/dpkginfo/CMakeLists.txt @@ -0,0 +1,3 @@ +if(OPENSCAP_PROBE_LINUX_DPKGINFO) + add_oscap_test("test_probes_dpkginfo.sh" LABELS linux linux_only) +endif() diff --git a/tests/probes/dpkginfo/test_probes_dpkginfo.sh b/tests/probes/dpkginfo/test_probes_dpkginfo.sh new file mode 100755 index 0000000000..654b671b4d --- /dev/null +++ b/tests/probes/dpkginfo/test_probes_dpkginfo.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +. $builddir/tests/test_common.sh + +set -e -o pipefail + +function test_probes_dpkginfo { + probecheck "dpkginfo" || return 255 + + local root_dir + local result + local ret_val=0 + + root_dir=$(mktemp -d) + result=$(mktemp) + mkdir -p "$root_dir/var/lib/dpkg" + + cat > "$root_dir/var/lib/dpkg/status" <<'EOF' +Package: alpha-tool +Status: install ok installed +Architecture: amd64 +Version: 1:1.2.3-4 +Provides: system-logger +Description: alpha tool + +Package: alpha-lib +Status: install ok installed +Architecture: all +Version: 2.0-1 +Description: alpha library + +Package: beta-tool +Status: install ok installed +Architecture: amd64 +Version: 3.0-1 +Description: beta tool + +Package: alpha-old +Status: deinstall ok config-files +Architecture: amd64 +Version: 0.1-1 +Description: removed alpha package +EOF + + OSCAP_PROBE_ROOT="$root_dir" $OSCAP oval eval --results "$result" "$srcdir/test_probes_dpkginfo.xml" + + if [ -f "$result" ]; then + verify_results "def" "$srcdir/test_probes_dpkginfo.xml" "$result" 4 && \ + verify_results "tst" "$srcdir/test_probes_dpkginfo.xml" "$result" 4 + ret_val=$? + else + ret_val=1 + fi + + assert_exists 1 '/oval_results/results/system/oval_system_characteristics/collected_objects/object[@id="oval:1:obj:1"]/reference' || ret_val=1 + assert_exists 2 '/oval_results/results/system/oval_system_characteristics/collected_objects/object[@id="oval:1:obj:2"]/reference' || ret_val=1 + assert_exists 3 '/oval_results/results/system/oval_system_characteristics/collected_objects/object[@id="oval:1:obj:3"]/reference' || ret_val=1 + assert_exists 1 '/oval_results/results/system/oval_system_characteristics/collected_objects/object[@id="oval:1:obj:4"]/reference' || ret_val=1 + + rm -rf "$root_dir" + rm -f "$result" + + return $ret_val +} + +test_init + +test_run "dpkginfo probe name operations" test_probes_dpkginfo + +test_exit diff --git a/tests/probes/dpkginfo/test_probes_dpkginfo.xml b/tests/probes/dpkginfo/test_probes_dpkginfo.xml new file mode 100644 index 0000000000..2538396353 --- /dev/null +++ b/tests/probes/dpkginfo/test_probes_dpkginfo.xml @@ -0,0 +1,97 @@ + + + + dpkginfo + 1.0 + 5.11.1 + 2026-05-11T00:00:00+00:00 + + + + + dpkginfo equals + Collect one exact package. + + + + + + + + dpkginfo pattern match + Collect installed packages matching a name pattern. + + + + + + + + dpkginfo not equal + Collect installed packages not matching one package name. + + + + + + + + dpkginfo virtual package + Collect an installed package through a provided virtual package name. + + + + + + + + + + + + + + + + + + + + + + + + + + + alpha-tool + + + ^alpha-.* + + + alpha-tool + + + system-logger + + + + + alpha-tool + + + ^alpha-.* + + + alpha-tool + + + system-logger + + +