From 76a87eebe10ad30b59645d4899542ca9a387e918 Mon Sep 17 00:00:00 2001 From: Stewart Smith Date: Tue, 21 May 2024 00:48:00 +0000 Subject: [PATCH] Add XML output option to 'dnf5 advisories list' Add an option to output the list of applicable advisories in the same XML format as the updateinfo.xml file in the repository. This allows tooling to have a consistent machine readable format for getting what advisories apply to a system. Resorting to parsing text output of commands is (naturally) error prone, and designed for humans to look at rather than machines. Alternatives for these tools include using the APIs of dnf/yum/other-rpm-package-manager, and keeping up with their evolution. For tooling that supports a wide range of distributions and versions, this can become rather onerous, especially across the many different security/patch management products. With a documented format, we can help these tools get the correct information both on available updates, and what ones are applicable to particular systems. Using the same format these tools need to be able to parse already avoids the need to get multiple parsers correct. Signed-off-by: Stewart Smith --- dnf5/commands/advisory/advisory_list.cpp | 19 ++- .../commands/advisory/advisory_subcommand.cpp | 1 + .../commands/advisory/advisory_subcommand.hpp | 1 + dnf5/commands/advisory/arguments.hpp | 5 + .../output/adapters/advisory_tmpl.hpp | 2 +- .../output/interfaces/advisory.hpp | 2 +- .../libdnf5-cli/output/xmladvisorylist.hpp | 39 ++++++ include/libdnf5/advisory/advisory_package.hpp | 5 + {libdnf5 => include/libdnf5}/utils/xml.hpp | 0 libdnf5-cli/CMakeLists.txt | 4 + libdnf5-cli/output/xmladvisorylist.cpp | 123 ++++++++++++++++++ libdnf5/advisory/advisory_package.cpp | 7 + libdnf5/advisory/advisory_package_private.hpp | 1 + libdnf5/comps/environment/environment.cpp | 2 +- libdnf5/comps/group/group.cpp | 2 +- libdnf5/repo/repo_sack.cpp | 2 +- libdnf5/utils/xml.cpp | 2 +- 17 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 include/libdnf5-cli/output/xmladvisorylist.hpp rename {libdnf5 => include/libdnf5}/utils/xml.hpp (100%) create mode 100644 libdnf5-cli/output/xmladvisorylist.cpp diff --git a/dnf5/commands/advisory/advisory_list.cpp b/dnf5/commands/advisory/advisory_list.cpp index 7ec507fe0..d81f3d2fa 100644 --- a/dnf5/commands/advisory/advisory_list.cpp +++ b/dnf5/commands/advisory/advisory_list.cpp @@ -21,6 +21,7 @@ along with libdnf. If not, see . #include #include +#include #include #include @@ -51,22 +52,26 @@ void AdvisoryListCommand::process_and_print_queries( packages.filter_upgradable(); not_installed_pkgs = advisories.get_advisory_packages_sorted(packages, libdnf5::sack::QueryCmp::GT); } else { // available is the default - libdnf5::rpm::PackageQuery installed_packages(ctx.get_base()); - installed_packages.filter_installed(); - installed_packages.filter_latest_evr(); + packages = libdnf5::rpm::PackageQuery(ctx.get_base()); + packages.filter_installed(); + packages.filter_latest_evr(); - add_running_kernel_packages(ctx.get_base(), installed_packages); + add_running_kernel_packages(ctx.get_base(), packages); if (package_specs.size() > 0) { - installed_packages.filter_name(package_specs, libdnf5::sack::QueryCmp::IGLOB); + packages.filter_name(package_specs, libdnf5::sack::QueryCmp::IGLOB); } - not_installed_pkgs = advisories.get_advisory_packages_sorted(installed_packages, libdnf5::sack::QueryCmp::GT); + not_installed_pkgs = advisories.get_advisory_packages_sorted(packages, libdnf5::sack::QueryCmp::GT); } std::vector> cli_installed_pkgs; std::vector> cli_not_installed_pkgs; - if (with_bz->get_value()) { + if (as_xml->get_value()) { + libdnf5::advisory::AdvisoryQuery advisory_subset(advisories); + advisory_subset.filter_packages(packages, libdnf5::sack::QueryCmp::GT); + libdnf5::cli::output::print_xmladvisorylist(advisory_subset); + } else if (with_bz->get_value()) { libdnf5::cli::output::print_advisorylist_references_table(not_installed_pkgs, installed_pkgs, "bugzilla"); } else if (with_cve->get_value()) { libdnf5::cli::output::print_advisorylist_references_table(not_installed_pkgs, installed_pkgs, "cve"); diff --git a/dnf5/commands/advisory/advisory_subcommand.cpp b/dnf5/commands/advisory/advisory_subcommand.cpp index e6e306876..706845509 100644 --- a/dnf5/commands/advisory/advisory_subcommand.cpp +++ b/dnf5/commands/advisory/advisory_subcommand.cpp @@ -44,6 +44,7 @@ void AdvisorySubCommand::set_argument_parser() { advisory_severity = std::make_unique(*this); advisory_bz = std::make_unique(*this); advisory_cve = std::make_unique(*this); + as_xml = std::make_unique(*this); with_bz = std::make_unique(*this); with_cve = std::make_unique(*this); diff --git a/dnf5/commands/advisory/advisory_subcommand.hpp b/dnf5/commands/advisory/advisory_subcommand.hpp index 2559950b2..9907e4565 100644 --- a/dnf5/commands/advisory/advisory_subcommand.hpp +++ b/dnf5/commands/advisory/advisory_subcommand.hpp @@ -52,6 +52,7 @@ class AdvisorySubCommand : public Command { std::unique_ptr advisory_specs{nullptr}; std::unique_ptr with_bz{nullptr}; std::unique_ptr with_cve{nullptr}; + std::unique_ptr as_xml{nullptr}; AdvisorySubCommand(Context & context, const std::string & name) : Command(context, name) {} diff --git a/dnf5/commands/advisory/arguments.hpp b/dnf5/commands/advisory/arguments.hpp index 646fb9994..b57311c12 100644 --- a/dnf5/commands/advisory/arguments.hpp +++ b/dnf5/commands/advisory/arguments.hpp @@ -90,6 +90,11 @@ class AdvisorySpecArguments : public libdnf5::cli::session::StringArgumentList { : StringArgumentList(command, "advisory-spec", _("List of patterns matched against advisory names.")) {} }; +class AdvisoryListAsXmlOption : public libdnf5::cli::session::BoolOption { +public: + explicit AdvisoryListAsXmlOption(libdnf5::cli::session::Command & command) + : BoolOption(command, "xml-output", '\0', _("Output advisory list in updateinfo.xml format."), false) {} +}; } // namespace dnf5 diff --git a/include/libdnf5-cli/output/adapters/advisory_tmpl.hpp b/include/libdnf5-cli/output/adapters/advisory_tmpl.hpp index f2859f328..ab2e74465 100644 --- a/include/libdnf5-cli/output/adapters/advisory_tmpl.hpp +++ b/include/libdnf5-cli/output/adapters/advisory_tmpl.hpp @@ -175,7 +175,7 @@ class AdvisoryAdapter : public IAdvisory { return ret; } - std::vector> get_collections() const override { + std::vector> get_collections() override { std::vector> ret; auto collections = pkg.get_collections(); ret.reserve(collections.size()); diff --git a/include/libdnf5-cli/output/interfaces/advisory.hpp b/include/libdnf5-cli/output/interfaces/advisory.hpp index 78f499cbb..1694a1080 100644 --- a/include/libdnf5-cli/output/interfaces/advisory.hpp +++ b/include/libdnf5-cli/output/interfaces/advisory.hpp @@ -94,7 +94,7 @@ class IAdvisory { virtual std::string get_rights() const = 0; virtual std::string get_message() const = 0; virtual std::vector> get_references() const = 0; - virtual std::vector> get_collections() const = 0; + virtual std::vector> get_collections() = 0; }; } // namespace libdnf5::cli::output diff --git a/include/libdnf5-cli/output/xmladvisorylist.hpp b/include/libdnf5-cli/output/xmladvisorylist.hpp new file mode 100644 index 000000000..fe03cbf07 --- /dev/null +++ b/include/libdnf5-cli/output/xmladvisorylist.hpp @@ -0,0 +1,39 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with libdnf. If not, see . +*/ + + +#ifndef LIBDNF5_CLI_OUTPUT_XMLADVISORYLIST_HPP +#define LIBDNF5_CLI_OUTPUT_XMLADVISORYLIST_HPP + +#include "interfaces/advisory.hpp" + +#include +#include +#include +#include +#include + +namespace libdnf5::cli::output { + +void print_xmladvisorylist( + libdnf5::advisory::AdvisoryQuery advisories); + +} // namespace libdnf5::cli::output + +#endif // LIBDNF5_CLI_OUTPUT_XMLADVISORYLIST_HPP diff --git a/include/libdnf5/advisory/advisory_package.hpp b/include/libdnf5/advisory/advisory_package.hpp index 5d64151ba..c5cc39906 100644 --- a/include/libdnf5/advisory/advisory_package.hpp +++ b/include/libdnf5/advisory/advisory_package.hpp @@ -79,6 +79,11 @@ class AdvisoryPackage { /// @return Arch of this AdvisoryPackage as std::string. std::string get_arch() const; + /// Get the filename of this AdvisoryPackage. + /// + /// @return Filename of this AdvisoryPackage as std::string. + std::string get_filename() const; + /// Get NEVRA of this AdvisoryPackage. /// /// @return NEVRA of this AdvisoryPackage as std::string. diff --git a/libdnf5/utils/xml.hpp b/include/libdnf5/utils/xml.hpp similarity index 100% rename from libdnf5/utils/xml.hpp rename to include/libdnf5/utils/xml.hpp diff --git a/libdnf5-cli/CMakeLists.txt b/libdnf5-cli/CMakeLists.txt index 8cfa9d1e0..10828222a 100644 --- a/libdnf5-cli/CMakeLists.txt +++ b/libdnf5-cli/CMakeLists.txt @@ -41,6 +41,10 @@ pkg_check_modules(SMARTCOLS REQUIRED smartcols) list(APPEND LIBDNF5_CLI_PC_REQUIRES_PRIVATE "${SMARTCOLS_MODULE_NAME}") target_link_libraries(libdnf5-cli PRIVATE ${SMARTCOLS_LIBRARIES}) +pkg_check_modules(LIBXML2 REQUIRED libxml-2.0) +list(APPEND LIBDNF5_PC_REQUIRES_PRIVATE "${LIBXML2_MODULE_NAME}") +include_directories(${LIBXML2_INCLUDE_DIRS}) +target_link_libraries(libdnf5 PRIVATE ${LIBXML2_LIBRARIES}) # sort the pkg-config requires and concatenate them into a string list(SORT LIBDNF5_CLI_PC_REQUIRES) diff --git a/libdnf5-cli/output/xmladvisorylist.cpp b/libdnf5-cli/output/xmladvisorylist.cpp new file mode 100644 index 000000000..a686ec52e --- /dev/null +++ b/libdnf5-cli/output/xmladvisorylist.cpp @@ -0,0 +1,123 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with libdnf. If not, see . +*/ + +#include "libdnf5-cli/output/xmladvisorylist.hpp" + +#include "utils/string.hpp" + +#include +#include +#include + +#include +#include + +#include "libdnf5/utils/bgettext/bgettext-mark-domain.h" + +namespace libdnf5::cli::output { + +namespace { + void add_advisory_to_node(xmlNodePtr node, auto & advisory) { + xmlNodePtr update_node = xmlNewChild(node, NULL, BAD_CAST "update", NULL); + xmlNewProp(update_node, BAD_CAST "from", BAD_CAST advisory.get_vendor().c_str()); + xmlNewProp(update_node, BAD_CAST "status", BAD_CAST advisory.get_status().c_str()); + xmlNewProp(update_node, BAD_CAST "type", BAD_CAST advisory.get_type().c_str()); + xmlNewProp(update_node, BAD_CAST "version", BAD_CAST "2.0"); + + utils::xml::add_subnode_with_text(update_node, "id", advisory.get_name()); + utils::xml::add_subnode_with_text(update_node, "severity", advisory.get_severity()); + utils::xml::add_subnode_with_text(update_node, "title", advisory.get_title()); + + std::string message = advisory.get_message(); + if (message.size() > 0) + utils::xml::add_subnode_with_text(update_node, "message", message); + + // libsolv uses the later of issued/updated, so just display as updated + // This is probably a FIXME + unsigned long long buildtime = advisory.get_buildtime(); + xmlNodePtr buildtime_node = xmlNewChild(update_node, NULL, BAD_CAST "updated", NULL); + xmlNewProp(buildtime_node, BAD_CAST "date", BAD_CAST libdnf5::utils::string::format_epoch(buildtime).c_str()); + + utils::xml::add_subnode_with_text(update_node, "description", advisory.get_description()); + + std::string rights = advisory.get_rights(); + if (rights.size() > 0) + utils::xml::add_subnode_with_text(update_node, "rights", rights); + + auto references = advisory.get_references(); + if (references.size() > 0) { + xmlNodePtr references_node = xmlNewChild(update_node, NULL, BAD_CAST "references", NULL); + for (auto & ref : advisory.get_references()) { + xmlNodePtr ref_node = xmlNewChild(references_node, NULL, BAD_CAST "reference", NULL); + xmlNewProp(ref_node, BAD_CAST "href", BAD_CAST ref.get_url().c_str()); + xmlNewProp(ref_node, BAD_CAST "id", BAD_CAST ref.get_id().c_str()); + xmlNewProp(ref_node, BAD_CAST "title", BAD_CAST ref.get_title().c_str()); + xmlNewProp(ref_node, BAD_CAST "type", BAD_CAST ref.get_type().c_str()); + } + } + auto collections = advisory.get_collections(); + if (collections.size() > 0) { + for (auto & collection : collections) { + auto pkgs = collection.get_packages(); + if (pkgs.size() > 0) { + xmlNodePtr pkglist_node = xmlNewChild(update_node, NULL, BAD_CAST "pkglist", NULL); + xmlNodePtr collection_node = xmlNewChild(pkglist_node, NULL, BAD_CAST "collection", NULL); + for (auto & pkg : pkgs) { + xmlNodePtr package_node = xmlNewChild(collection_node, NULL, BAD_CAST "package", NULL); + xmlNewProp(package_node, BAD_CAST "name", BAD_CAST pkg.get_name().c_str()); + xmlNewProp(package_node, BAD_CAST "version", BAD_CAST pkg.get_version().c_str()); + xmlNewProp(package_node, BAD_CAST "release", BAD_CAST pkg.get_release().c_str()); + xmlNewProp(package_node, BAD_CAST "epoch", BAD_CAST pkg.get_epoch().c_str()); + xmlNewProp(package_node, BAD_CAST "arch", BAD_CAST pkg.get_arch().c_str()); + std::string filename = pkg.get_filename(); + if (filename.size() > 0) + xmlNewChild(package_node, NULL, BAD_CAST "filename", BAD_CAST filename.c_str()); + if (pkg.get_reboot_suggested()) + xmlNewChild(package_node, NULL, BAD_CAST "reboot_suggested", BAD_CAST "True"); + if (pkg.get_restart_suggested()) + xmlNewChild(package_node, NULL, BAD_CAST "restart_suggested", BAD_CAST "True"); + if (pkg.get_relogin_suggested()) + xmlNewChild(package_node, NULL, BAD_CAST "relogin_suggested", BAD_CAST "True"); + } + } + } + } + } +} + +void print_xmladvisorylist( + libdnf5::advisory::AdvisoryQuery advisories) { + xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0"); + xmlNodePtr node_updates = xmlNewNode(NULL, BAD_CAST "updates"); + xmlDocSetRootElement(doc, node_updates); + + for (auto advisory : advisories) { + add_advisory_to_node(node_updates, advisory); + } + + xmlChar *mem; + int sz; + xmlDocDumpFormatMemory(doc, &mem, &sz, 1); + std::cout << (char*)mem << std::endl; + xmlFree(mem); + + xmlFreeDoc(doc); +} + +} diff --git a/libdnf5/advisory/advisory_package.cpp b/libdnf5/advisory/advisory_package.cpp index 776297375..933956092 100644 --- a/libdnf5/advisory/advisory_package.cpp +++ b/libdnf5/advisory/advisory_package.cpp @@ -64,6 +64,9 @@ std::string AdvisoryPackage::get_arch() const { std::string AdvisoryPackage::get_nevra() const { return p_impl->get_name() + "-" + p_impl->get_evr() + "." + p_impl->get_arch(); } +std::string AdvisoryPackage::get_filename() const { + return p_impl->get_filename(); +} AdvisoryId AdvisoryPackage::get_advisory_id() const { return p_impl->get_advisory_id(); } @@ -133,6 +136,10 @@ std::string AdvisoryPackage::Impl::get_arch() const { return get_rpm_pool(base).id2str(arch); } +std::string AdvisoryPackage::Impl::get_filename() const { + return filename; +} + bool AdvisoryPackage::Impl::is_resolved_in(const libdnf5::rpm::PackageSet & pkgs) const { auto & pool = get_rpm_pool(base); auto sack = base->get_rpm_package_sack(); diff --git a/libdnf5/advisory/advisory_package_private.hpp b/libdnf5/advisory/advisory_package_private.hpp index f253f2c3c..78afd566a 100644 --- a/libdnf5/advisory/advisory_package_private.hpp +++ b/libdnf5/advisory/advisory_package_private.hpp @@ -41,6 +41,7 @@ class AdvisoryPackage::Impl { std::string get_release() const; std::string get_evr() const; std::string get_arch() const; + std::string get_filename() const; Id get_name_id() const { return name; }; Id get_evr_id() const { return evr; }; Id get_arch_id() const { return arch; }; diff --git a/libdnf5/comps/environment/environment.cpp b/libdnf5/comps/environment/environment.cpp index fe3b4a363..2f6f782c0 100644 --- a/libdnf5/comps/environment/environment.cpp +++ b/libdnf5/comps/environment/environment.cpp @@ -20,7 +20,7 @@ along with libdnf. If not, see . #include "libdnf5/comps/environment/environment.hpp" #include "solv/pool.hpp" -#include "utils/xml.hpp" +#include "libdnf5/utils/xml.hpp" #include "libdnf5/base/base.hpp" #include "libdnf5/comps/environment/query.hpp" diff --git a/libdnf5/comps/group/group.cpp b/libdnf5/comps/group/group.cpp index 173bab6f4..ef1b596ca 100644 --- a/libdnf5/comps/group/group.cpp +++ b/libdnf5/comps/group/group.cpp @@ -21,7 +21,7 @@ along with libdnf. If not, see . #include "solv/pool.hpp" #include "utils/string.hpp" -#include "utils/xml.hpp" +#include "libdnf5/utils/xml.hpp" #include "libdnf5/base/base.hpp" #include "libdnf5/comps/group/package.hpp" diff --git a/libdnf5/repo/repo_sack.cpp b/libdnf5/repo/repo_sack.cpp index 5667697f3..b2da0dbec 100644 --- a/libdnf5/repo/repo_sack.cpp +++ b/libdnf5/repo/repo_sack.cpp @@ -30,7 +30,7 @@ along with libdnf. If not, see . #include "utils/fs/utils.hpp" #include "utils/string.hpp" #include "utils/url.hpp" -#include "utils/xml.hpp" +#include "libdnf5/utils/xml.hpp" #include "libdnf5/base/base.hpp" #include "libdnf5/common/exception.hpp" diff --git a/libdnf5/utils/xml.cpp b/libdnf5/utils/xml.cpp index 185c24695..8f19327c9 100644 --- a/libdnf5/utils/xml.cpp +++ b/libdnf5/utils/xml.cpp @@ -18,7 +18,7 @@ along with libdnf. If not, see . */ -#include "xml.hpp" +#include #include