From 79f329583d7e297358081c55b0b6f8a04f5e08cf Mon Sep 17 00:00:00 2001 From: Ariel Martin Date: Fri, 27 Jun 2025 17:38:13 -0300 Subject: [PATCH 1/6] feat: modifies makelists --- src/modules/sca/CMakeLists.txt | 10 +++++----- src/vcpkg.json | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/modules/sca/CMakeLists.txt b/src/modules/sca/CMakeLists.txt index bcff3cac16..f72e8e91bd 100644 --- a/src/modules/sca/CMakeLists.txt +++ b/src/modules/sca/CMakeLists.txt @@ -7,11 +7,10 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# include(../../cmake/CommonSettings.cmake) -# set_common_settings() +# include(../../cmake/CommonSettings.cmake) set_common_settings() find_package(pcre2 CONFIG REQUIRED) -find_package(yaml-cpp CONFIG REQUIRED) +find_package(yaml CONFIG REQUIRED) add_library( SCA @@ -37,12 +36,13 @@ target_link_libraries( sysinfo # available in 4.x, uses processes method cmd_helper # Utils::exec differs from 6.x to 4.x, needs to be adapted nlohmann_json::nlohmann_json - PRIVATE yaml-cpp::yaml-cpp + PRIVATE yaml PCRE2::8BIT hash_helper # uses Utils::HashData class available in 4.x hashHelper.h string_helper # uses Utils::Trim and Utils::asciiToHex (available in 4.x stringHelper.h) time_helper # uses Utils::getCurrentISO8601 available in 4.x timeHelper.h - $<$:registry_helper>) # uses Utils::Registry class (available) and Utils::KeyExists (not available) + $<$:registry_helper>) # uses Utils::Registry class (available) and Utils::KeyExists + # (not available) include(../../cmake/ConfigureTarget.cmake) configure_target(SCA) diff --git a/src/vcpkg.json b/src/vcpkg.json index 36f7a29456..982215d947 100644 --- a/src/vcpkg.json +++ b/src/vcpkg.json @@ -97,6 +97,10 @@ { "name": "yaml-cpp", "version>=": "0.8.0" + }, + { + "name": "libyaml", + "version>=": "0.2.5#5" } ], "vcpkg-configuration": { From e629d9d59cf4473d134e2d7e865e9a29cf5919ce Mon Sep 17 00:00:00 2001 From: Ariel Martin Date: Mon, 7 Jul 2025 21:21:22 -0300 Subject: [PATCH 2/6] feat: adds IYamlDocument interface --- src/modules/sca/src/iyaml_document.hpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/modules/sca/src/iyaml_document.hpp diff --git a/src/modules/sca/src/iyaml_document.hpp b/src/modules/sca/src/iyaml_document.hpp new file mode 100644 index 0000000000..e9ac8e8ac2 --- /dev/null +++ b/src/modules/sca/src/iyaml_document.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "yaml_node.hpp" + +/// @brief Interface for YAML document class +class IYamlDocument +{ +public: + /// @brief Destructor + virtual ~IYamlDocument() = default; + + /// @brief Gets the root node of the YAML document + /// @return YamlNode object representing the root node + virtual YamlNode GetRoot() = 0; + + /// @brief Checks if the YAML document has been loaded with no errors + /// @return True if the document has been successfully loaded, false otherwise + virtual bool IsValidDocument() const = 0; +}; From 921f9043da951b6674f9955a9425ade3d167ecc6 Mon Sep 17 00:00:00 2001 From: Ariel Martin Date: Sat, 5 Jul 2025 23:17:17 -0300 Subject: [PATCH 3/6] feat: adds the libyaml c++ wrapper --- src/modules/sca/CMakeLists.txt | 2 + src/modules/sca/src/yaml_document.cpp | 68 +++++ src/modules/sca/src/yaml_document.hpp | 47 ++++ src/modules/sca/src/yaml_node.cpp | 377 ++++++++++++++++++++++++++ src/modules/sca/src/yaml_node.hpp | 144 ++++++++++ 5 files changed, 638 insertions(+) create mode 100644 src/modules/sca/src/yaml_document.cpp create mode 100644 src/modules/sca/src/yaml_document.hpp create mode 100644 src/modules/sca/src/yaml_node.cpp create mode 100644 src/modules/sca/src/yaml_node.hpp diff --git a/src/modules/sca/CMakeLists.txt b/src/modules/sca/CMakeLists.txt index f72e8e91bd..7f7c3fc8d4 100644 --- a/src/modules/sca/CMakeLists.txt +++ b/src/modules/sca/CMakeLists.txt @@ -15,6 +15,8 @@ find_package(yaml CONFIG REQUIRED) add_library( SCA src/sca.cpp + src/yaml_document.cpp + src/yaml_node.cpp src/sca_policy.cpp src/sca_policy_check.cpp src/sca_policy_loader.cpp diff --git a/src/modules/sca/src/yaml_document.cpp b/src/modules/sca/src/yaml_document.cpp new file mode 100644 index 0000000000..7bad2f2f17 --- /dev/null +++ b/src/modules/sca/src/yaml_document.cpp @@ -0,0 +1,68 @@ + +#include "yaml_document.hpp" +#include "yaml_node.hpp" + +#include + +#include + +YamlDocument::YamlDocument() + : m_loaded(false) {}; + +YamlDocument::YamlDocument(const std::filesystem::path& filename) +{ + std::ifstream file(filename.c_str()); + if (!file) + { + throw std::runtime_error("Cannot open file"); + }; + + if (!Load(file)) + { + // TODO: Log("Failed to parse YAML document:" filename.string().c_str()); + } +} + +YamlDocument::YamlDocument(const std::string& yaml_content) +{ + std::istringstream ss(yaml_content); + + if (!Load(ss)) + { + // TODO: Log("Failed to parse YAML content"); + } +} + +YamlDocument::~YamlDocument() +{ + if (m_loaded) + { + yaml_document_delete(&m_document); + } + yaml_parser_delete(&m_parser); +} + +bool YamlDocument::IsValidDocument() const +{ + return m_loaded; +} + +bool YamlDocument::Load(std::istream& input) +{ + yaml_parser_initialize(&m_parser); + std::string content((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + yaml_parser_set_input_string(&m_parser, reinterpret_cast(content.c_str()), content.size()); + m_loaded = yaml_parser_load(&m_parser, &m_document); + + return m_loaded; +} + +YamlNode YamlDocument::GetRoot() +{ + auto root = yaml_document_get_root_node(&m_document); + if (!root) + { + throw std::runtime_error("Empty YAML document"); + } + return YamlNode(&m_document, root); +} diff --git a/src/modules/sca/src/yaml_document.hpp b/src/modules/sca/src/yaml_document.hpp new file mode 100644 index 0000000000..9b6b1f7b13 --- /dev/null +++ b/src/modules/sca/src/yaml_document.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include +#include + +/// @brief Class representing a YAML document +class YamlDocument : public IYamlDocument +{ +public: + /// @brief Constructor for loading a YAML document from a file + YamlDocument(const std::filesystem::path& filename); + + /// @brief Constructor for loading a YAML document from a string + YamlDocument(const std::string& yaml_content); + + /// @brief Default constructor + YamlDocument(); + + /// @brief Destructor + ~YamlDocument(); + + /// @copydoc IYamlDocument::GetRoot + YamlNode GetRoot() override; + + /// @copydoc IYamlDocument::IsValidDocument + bool IsValidDocument() const override; + + friend class YamlNode; + +private: + /// @brief yaml_parser_t object used to parse the YAML document + yaml_parser_t m_parser; + + /// @brief Flag indicating whether the document has been loaded + bool m_loaded; + + /// @brief yaml_document_t object representing the parsed YAML document + yaml_document_t m_document; + + /// @brief Loads the YAML document from the given input stream + /// @param input The input stream to load the document from + /// @return True if the document was successfully loaded, false otherwise + bool Load(std::istream& input); +}; diff --git a/src/modules/sca/src/yaml_node.cpp b/src/modules/sca/src/yaml_node.cpp new file mode 100644 index 0000000000..536243f6a6 --- /dev/null +++ b/src/modules/sca/src/yaml_node.cpp @@ -0,0 +1,377 @@ +#include "yaml_node.hpp" +#include "yaml_document.hpp" + +#include + +YamlNode::YamlNode(yaml_document_t* doc, yaml_node_t* nodePtr) + : m_document(doc) + , m_node(nodePtr) +{ + if (!m_node) + { + return; + } + + switch (m_node->type) + { + case YAML_SCALAR_NODE: m_type = Type::Scalar; break; + case YAML_SEQUENCE_NODE: m_type = Type::Sequence; break; + case YAML_MAPPING_NODE: m_type = Type::Mapping; break; + case YAML_NO_NODE: m_type = Type::Undefined; break; + default: m_type = Type::Undefined; break; + } +} + +void YamlNode::DumpYamlStructure(unsigned int indent) const +{ + std::string padding(indent, ' '); + std::cout << padding << "Node Type: " << GetNodeTypeAsString(); + if (IsScalar()) + { + std::cout << ", Value: " << AsString(); + } + std::cout << std::endl; + + if (IsMap()) + { + for (const auto& [key, val] : AsMap()) + { + std::cout << padding << "- Key: " << key << std::endl; + val.DumpYamlStructure(indent + 2); + } + } + else if (IsSequence()) + { + auto seq = AsSequence(); + for (size_t i = 0; i < seq.size(); ++i) + { + std::cout << padding << "- Index [" << i << "]" << std::endl; + seq[i].DumpYamlStructure(indent + 2); + } + } +} + +YamlNode::Type YamlNode::GetNodeType() const +{ + return m_type; +} + +std::string YamlNode::GetNodeTypeAsString() const +{ + switch (m_type) + { + case Type::Scalar: return "Scalar"; + case Type::Sequence: return "Sequence"; + case Type::Mapping: return "Mapping"; + case Type::Undefined: // fallthrough + default: return "Undefined"; + } +} + +bool YamlNode::IsScalar() const +{ + return m_type == Type::Scalar; +} + +bool YamlNode::IsSequence() const +{ + return m_type == Type::Sequence; +} + +bool YamlNode::IsMap() const +{ + return m_type == Type::Mapping; +} + +std::string YamlNode::AsString() const +{ + if (!IsScalar()) + { + throw std::runtime_error("Node is not a scalar"); + } + return std::string(reinterpret_cast(m_node->data.scalar.value)); +} + +std::vector YamlNode::AsSequence() const +{ + if (!IsSequence()) + { + throw std::runtime_error("Node is not a sequence"); + } + if (!sequence_cache.empty()) + { + return sequence_cache; + } + + for (yaml_node_item_t* item = m_node->data.sequence.items.start; item < m_node->data.sequence.items.top; ++item) + { + sequence_cache.emplace_back(m_document, yaml_document_get_node(m_document, *item)); + } + return sequence_cache; +} + +std::map YamlNode::AsMap() const +{ + if (!IsMap()) + { + throw std::runtime_error("Node is not a map"); + } + if (!map_cache.empty()) + { + return map_cache; + } + + for (yaml_node_pair_t* pair = m_node->data.mapping.pairs.start; pair < m_node->data.mapping.pairs.top; ++pair) + { + auto key_node = yaml_document_get_node(m_document, pair->key); + auto val_node = yaml_document_get_node(m_document, pair->value); + if (key_node && key_node->type == YAML_SCALAR_NODE) + { + std::string key = reinterpret_cast(key_node->data.scalar.value); + map_cache[key] = YamlNode(m_document, val_node); + } + } + return map_cache; +} + +const YamlNode& YamlNode::operator[](const std::string& key) const +{ + if (!IsMap()) + { + throw std::runtime_error("Not a map node"); + } + const auto map = AsMap(); + if (map.find(key) == map.end()) + { + throw std::out_of_range("Key not found"); + } + return map_cache[key]; +} + +const YamlNode& YamlNode::operator[](size_t index) const +{ + if (!IsSequence()) + { + throw std::runtime_error("Not a sequence node"); + } + const auto seq = AsSequence(); + if (index >= seq.size()) + { + throw std::out_of_range("Index out of bounds"); + } + return sequence_cache[index]; +} + +YamlNode& YamlNode::operator[](const std::string& key) +{ + if (!IsMap()) + { + throw std::runtime_error("Not a map node"); + } + const auto map = AsMap(); + if (map.find(key) == map.end()) + { + throw std::out_of_range("Key not found"); + } + return map_cache[key]; +} + +YamlNode& YamlNode::operator[](size_t index) +{ + if (!IsSequence()) + { + throw std::runtime_error("Not a sequence node"); + } + const auto seq = AsSequence(); + if (index >= seq.size()) + { + throw std::out_of_range("Index out of bounds"); + } + return sequence_cache[index]; +} + +void YamlNode::SetScalarValue(const std::string& new_value) +{ + if (!IsScalar()) + { + throw std::runtime_error("Not a scalar node"); + } + + // Free the old string + free(m_node->data.scalar.value); + + // This new string will be owned and freed by the document on destruction + const auto newValue = strdup(new_value.c_str()); + + m_node->data.scalar.value = reinterpret_cast(newValue); + m_node->data.scalar.length = new_value.size(); +} + +bool YamlNode::HasKey(const std::string& key) const +{ + if (IsMap()) + { + const auto& map = AsMap(); + return map.find(key) != map.end(); + } + else if (IsSequence()) + { + const auto seq = AsSequence(); + for (const auto& item : seq) + { + if (item.IsMap()) + { + const auto& itemMap = item.AsMap(); + if (itemMap.find(key) != itemMap.end()) + { + return true; + } + } + } + return false; + } + return false; +} + +void YamlNode::RemoveKey(const std::string& key) +{ + if (!IsMap()) + { + throw std::runtime_error("Not a map node"); + } + yaml_node_t* const map_node = m_node; + + yaml_node_pair_t* out = map_node->data.mapping.pairs.start; + for (yaml_node_pair_t* in = map_node->data.mapping.pairs.start; in < map_node->data.mapping.pairs.top; ++in) + { + const auto key_node = yaml_document_get_node(m_document, in->key); + std::string current_key = reinterpret_cast(key_node->data.scalar.value); + if (current_key != key) + { + *out++ = *in; + } + } + map_node->data.mapping.pairs.top = out; + map_cache.clear(); +} + +int YamlNode::GetId() const +{ + for (int i = 1; i <= m_document->nodes.top - m_document->nodes.start; ++i) + { + const yaml_node_t* const candidate = yaml_document_get_node(m_document, i); + if (candidate == m_node) + { + return i; + } + } + throw std::runtime_error("Node not found in document"); +}; + +YamlNode YamlNode::CreateEmptySequence(const std::string& key) +{ + if (!IsMap()) + { + throw std::runtime_error("Not a map node"); + } + + yaml_char_t* const key_str = reinterpret_cast(strdup(key.c_str())); + const yaml_char_t* tag = reinterpret_cast(YAML_STR_TAG); + const int key_id = + yaml_document_add_scalar(m_document, tag, key_str, static_cast(key.length()), YAML_PLAIN_SCALAR_STYLE); + + // Free the key string - yaml_document_add_scalar will make its own copy + free(key_str); + + const int seq_id = yaml_document_add_sequence(m_document, tag, YAML_BLOCK_SEQUENCE_STYLE); + + const int mapping_id = GetId(); + + yaml_document_append_mapping_pair(m_document, mapping_id, key_id, seq_id); + + // important to clear our cache here + map_cache.clear(); + + yaml_node_t* const sequence_node = yaml_document_get_node(m_document, seq_id); + return YamlNode(m_document, sequence_node); +} + +void YamlNode::AppendToSequence(const std::string& value) +{ + if (!IsSequence()) + { + throw std::runtime_error("Not a sequence node"); + } + + yaml_char_t* const val_str = reinterpret_cast(strdup(value.c_str())); + const int scalar_id = yaml_document_add_scalar(m_document, + reinterpret_cast(YAML_STR_TAG), + val_str, + static_cast(value.length()), + YAML_PLAIN_SCALAR_STYLE); + + free(val_str); + + yaml_document_append_sequence_item(m_document, GetId(), scalar_id); + sequence_cache.clear(); +} + +YamlNode YamlNode::CloneInto(yaml_document_t* dest_doc) const +{ + switch (GetNodeType()) + { + case Type::Scalar: + { + const std::string value = AsString(); + yaml_char_t* const val_str = reinterpret_cast(strdup(value.c_str())); + const int scalar_id = yaml_document_add_scalar(dest_doc, + reinterpret_cast(YAML_STR_TAG), + val_str, + static_cast(value.length()), + YAML_PLAIN_SCALAR_STYLE); + free(val_str); + return YamlNode(dest_doc, yaml_document_get_node(dest_doc, scalar_id)); + } + case Type::Sequence: + { + const int seq_id = yaml_document_add_sequence( + dest_doc, reinterpret_cast(YAML_SEQ_TAG), YAML_BLOCK_SEQUENCE_STYLE); + for (const auto& item : AsSequence()) + { + const YamlNode cloned = item.CloneInto(dest_doc); + yaml_document_append_sequence_item(dest_doc, seq_id, cloned.GetId()); + } + return YamlNode(dest_doc, yaml_document_get_node(dest_doc, seq_id)); + } + case Type::Mapping: + { + const int map_id = yaml_document_add_mapping( + dest_doc, reinterpret_cast(YAML_MAP_TAG), YAML_BLOCK_MAPPING_STYLE); + for (const auto& [key, val] : AsMap()) + { + yaml_char_t* const key_str = reinterpret_cast(strdup(key.c_str())); + const int key_id = yaml_document_add_scalar(dest_doc, + reinterpret_cast(YAML_STR_TAG), + key_str, + static_cast(key.length()), + YAML_PLAIN_SCALAR_STYLE); + free(key_str); + const YamlNode cloned_val = val.CloneInto(dest_doc); + yaml_document_append_mapping_pair(dest_doc, map_id, key_id, cloned_val.GetId()); + } + return YamlNode(dest_doc, yaml_document_get_node(dest_doc, map_id)); + } + case Type::Undefined: // fallthrough + default: throw std::runtime_error("Unsupported or undefined node type"); + } +} + +YamlDocument YamlNode::Clone() const +{ + YamlDocument new_doc; + yaml_document_initialize(&new_doc.m_document, nullptr, nullptr, nullptr, 0, 0); + yaml_parser_initialize(&new_doc.m_parser); + new_doc.m_loaded = true; + CloneInto(&new_doc.m_document); + return new_doc; +} diff --git a/src/modules/sca/src/yaml_node.hpp b/src/modules/sca/src/yaml_node.hpp new file mode 100644 index 0000000000..de704ac0dd --- /dev/null +++ b/src/modules/sca/src/yaml_node.hpp @@ -0,0 +1,144 @@ + +#pragma once + +#include + +#include +#include +#include + +class YamlDocument; + +/// @brief Class representing a YAML node +class YamlNode +{ +public: + /// @brief Type of a YAML node + enum class Type + { + Scalar, + Sequence, + Mapping, + Undefined + }; + + /// @brief Constructor for a YAML node + /// @param doc Pointer to the yaml_document_t object parenting this node + /// @param node Pointer to the yaml_node_t this object represents + YamlNode(yaml_document_t* doc, yaml_node_t* node); + + /// @brief Default constructor + YamlNode() = default; + + /// @brief Gets the type of the YAML node + /// @return Type of the node + Type GetNodeType() const; + + /// @brief Gets the type of the YAML node as a string + /// @return String representation of the node type + std::string GetNodeTypeAsString() const; + + /// @brief Checks if the YAML node is a scalar + /// @return True if the node is a scalar, false otherwise + bool IsScalar() const; + + /// @brief Checks if the YAML node is a sequence + /// @return True if the node is a sequence, false otherwise + bool IsSequence() const; + + /// @brief Checks if the YAML node is a map + /// @return True if the node is a map, false otherwise + bool IsMap() const; + + /// @brief Gets the ID (or index) of the YAML node + /// @details For nodes loaded from a file, there is no way to get the ID other than + /// traversing the YAML document comparing the nodes and returning the corresponding index. + /// @return ID of the node + int GetId() const; + + /// @brief Gets the value of the YAML node as a string + /// @return String value of the node + std::string AsString() const; + + /// @brief Gets the value of the YAML node as a sequence + /// @return Vector of YamlNode objects representing the sequence + std::vector AsSequence() const; + + /// @brief Gets the value of the YAML node as a map + /// @return Map of YamlNode objects representing the map + std::map AsMap() const; + + /// @brief Gets the value of the YAML node (mapping only) + /// @param key Key of the map to get + /// @return YamlNode object representing the value + const YamlNode& operator[](const std::string& key) const; + + /// @brief Gets the value of the YAML node (sequence only) + /// @param index Index of the sequence to get + /// @return YamlNode object representing the value + const YamlNode& operator[](size_t index) const; + + /// @brief Gets the value of the YAML node (mapping only) + /// @param key Key of the map to get + /// @return YamlNode object representing the value + YamlNode& operator[](const std::string& key); + + /// @brief Gets the value of the YAML node (sequence only) + /// @param index Index of the sequence to get + /// @return YamlNode object representing the value + YamlNode& operator[](size_t index); + + /// @brief Sets the value of the YAML node + /// @param new_value New value to set + void SetScalarValue(const std::string& new_value); + + /// @brief Checks if the YAML node has a key + /// @param key Key to check + /// @return True if the key exists, false otherwise + bool HasKey(const std::string& key) const; + + /// @brief Removes a key from the YAML node + /// @param key Key to remove + void RemoveKey(const std::string& key); + + /// @brief Creates an empty sequence in the YAML node + /// @param key Key to create the sequence under + /// @return YamlNode object representing the sequence + YamlNode CreateEmptySequence(const std::string& key); + + /// @brief Appends a value to the YAML node sequence + /// @param value Value to append + void AppendToSequence(const std::string& value); + + /// @brief Dump the YAML structure to the console for debugging purposes + /// @param indent Indentation level + void DumpYamlStructure(unsigned int indent = 2) const; + + /// @brief Clones the YAML node + /// @return YamlDocument object holding the cloned node + YamlDocument Clone() const; + +private: + /// @brief Clones the YAML node into a new document + /// @param dest_doc Document to clone the node into + /// @return YamlNode object representing the cloned node + YamlNode CloneInto(yaml_document_t* dest_doc) const; + + /// @brief The underlying yaml_document_t + yaml_document_t* m_document = nullptr; + + /// @brief The underlying yaml_node_t + yaml_node_t* m_node = nullptr; + + /// @brief The type of the YAML node + Type m_type = Type::Undefined; + + /// @brief Cache for mapping nodes + mutable std::map map_cache; + + /// @brief Cache for sequence nodes + mutable std::vector sequence_cache; + + /// @brief Cache for scalar nodes + mutable std::string scalar_cache; +}; From e30de9d9282531085778bfd20254e7f31b84d9a9 Mon Sep 17 00:00:00 2001 From: Ariel Martin Date: Sat, 5 Jul 2025 22:14:10 -0300 Subject: [PATCH 4/6] refactor: changes module to use libyaml wrapper --- src/modules/sca/src/sca_policy_loader.cpp | 2 +- src/modules/sca/src/sca_policy_parser.cpp | 146 ++++++++++++---------- src/modules/sca/src/sca_policy_parser.hpp | 31 ++--- 3 files changed, 86 insertions(+), 93 deletions(-) diff --git a/src/modules/sca/src/sca_policy_loader.cpp b/src/modules/sca/src/sca_policy_loader.cpp index 3845479302..3fd6e2bb36 100644 --- a/src/modules/sca/src/sca_policy_loader.cpp +++ b/src/modules/sca/src/sca_policy_loader.cpp @@ -66,7 +66,7 @@ std::vector> SCAPolicyLoader::LoadPolicies(const Cre { // LogDebug("Loading policy from {}", path.string()); - const PolicyParser parser(path); + PolicyParser parser(path); if (auto policy = parser.ParsePolicy(policiesAndChecks); policy) { diff --git a/src/modules/sca/src/sca_policy_parser.cpp b/src/modules/sca/src/sca_policy_parser.cpp index 3c765e4053..a1a10c5fd5 100644 --- a/src/modules/sca/src/sca_policy_parser.cpp +++ b/src/modules/sca/src/sca_policy_parser.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include + // #include #include @@ -28,32 +31,32 @@ namespace } // NOLINTNEXTLINE(misc-no-recursion) - nlohmann::json YamlNodeToJson(const YAML::Node& yamlNode) + nlohmann::json YamlNodeToJson(const YamlNode& yamlNode) { if (yamlNode.IsScalar()) { - return yamlNode.as(); + return yamlNode.AsString(); } else if (yamlNode.IsSequence()) { std::vector values; - for (const auto& item : yamlNode) + const auto items = yamlNode.AsSequence(); + for (const auto& item : items) { if (item.IsScalar()) { - values.push_back(item.as()); + values.push_back(item.AsString()); } else if (item.IsMap()) { - for (const auto& subitem : item) + for (const auto& [key, subitem] : item.AsMap()) { - const auto key = subitem.first.as(); - const YAML::Node& valNode = subitem.second; - if (valNode.IsSequence()) + if (subitem.IsSequence()) { - for (const auto& val : valNode) + const auto subitems = subitem.AsSequence(); + for (const auto& val : subitems) { - values.emplace_back(key + ":" + val.as()); + values.emplace_back(key + ":" + val.AsString()); } } } @@ -64,9 +67,9 @@ namespace else if (yamlNode.IsMap()) { nlohmann::json j; - for (const auto& kv : yamlNode) + for (const auto& [key, node] : yamlNode.AsMap()) { - j[kv.first.as()] = YamlNodeToJson(kv.second); + j[key] = YamlNodeToJson(node); } return j; } @@ -84,28 +87,37 @@ namespace } // namespace // NOLINTNEXTLINE(performance-unnecessary-value-param) -PolicyParser::PolicyParser(const std::filesystem::path& filename, LoadFileFunc loadFileFunc) - : m_loadFileFunc(loadFileFunc ? std::move(loadFileFunc) : YAML::LoadFile) +PolicyParser::PolicyParser(const std::filesystem::path& filename, std::unique_ptr yamlDocument) { + if (yamlDocument) + { + m_yamlDocument = std::move(yamlDocument); + } + else + { + m_yamlDocument = std::make_unique(filename); + } + try { - if (!IsValidYamlFile(filename.string())) + if (!m_yamlDocument->IsValidDocument()) { throw std::runtime_error("The file does not contain a valid YAML structure."); } - m_node = m_loadFileFunc(filename.string()); + YamlNode root = m_yamlDocument->GetRoot(); - if (auto variables = m_node["variables"]; variables) + if (root.HasKey("variables")) { - for (const auto& var : variables) + const auto variablesNode = root["variables"]; + + for (const auto& [key, val] : variablesNode.AsMap()) { - const auto name = var.first.as(); - const auto value = var.second.as(); - m_variablesMap[name] = value; + m_variablesMap[key] = val.AsString(); } + + ReplaceVariablesInNode(root); } - ReplaceVariablesInNode(m_node); } catch (const std::exception& e) { @@ -113,41 +125,26 @@ PolicyParser::PolicyParser(const std::filesystem::path& filename, LoadFileFunc l } } -bool PolicyParser::IsValidYamlFile(const std::filesystem::path& filename) const -{ - try - { - const auto mapToValidte = m_loadFileFunc(filename.string()); - - if (!mapToValidte.IsMap() && !mapToValidte.IsSequence()) - { - throw std::runtime_error("The file does not contain a valid YAML structure."); - } - return true; - } - catch (const std::exception&) - { - return false; - } -} - -std::unique_ptr PolicyParser::ParsePolicy(nlohmann::json& policiesAndChecks) const +std::unique_ptr PolicyParser::ParsePolicy(nlohmann::json& policiesAndChecks) { std::vector checks; Check requirements; std::string policyId; - if (const auto policyNode = m_node["policy"]; policyNode) + const YamlNode root = m_yamlDocument->GetRoot(); + + if (root.HasKey("policy")) { try { - policyId = policyNode["id"].as(); + const auto policyNode = root["policy"]; + policyId = policyNode["id"].AsString(); policiesAndChecks["policies"].push_back(YamlNodeToJson(policyNode)); // LogDebug("Policy parsed."); } - catch (const YAML::Exception& e) + catch (const std::exception& e) { // LogError("Failed to parse policy. Skipping it. Error: {}", e.what()); } @@ -158,24 +155,26 @@ std::unique_ptr PolicyParser::ParsePolicy(nlohmann::json& policiesAn return nullptr; } - if (const auto requirementsNode = m_node["requirements"]; requirementsNode) + if (root.HasKey("requirements")) { try { - requirements.condition = requirementsNode["condition"].as(); + const auto requirementsNode = root["requirements"]; + requirements.condition = requirementsNode["condition"].AsString(); ValidateConditionString(requirements.condition); - for (const auto& rule : requirementsNode["rules"]) + const auto rules = requirementsNode["rules"].AsSequence(); + + for (const auto& rule : rules) { - std::unique_ptr RuleEvaluator = - RuleEvaluatorFactory::CreateEvaluator(rule.as()); + std::unique_ptr RuleEvaluator = RuleEvaluatorFactory::CreateEvaluator(rule.AsString()); if (RuleEvaluator != nullptr) { requirements.rules.push_back(std::move(RuleEvaluator)); } else { - // LogError("Failed to parse rule: {}", rule.as()); + // LogError("Failed to parse rule: {}", rule.AsString()); } } // LogDebug("Requirements parsed."); @@ -187,28 +186,39 @@ std::unique_ptr PolicyParser::ParsePolicy(nlohmann::json& policiesAn } } - if (const auto checksNode = m_node["checks"]; checksNode) + if (root.HasKey("checks")) { + const auto checksNode = root["checks"].AsSequence(); for (const auto& checkNode : checksNode) { try { Check check; - check.id = checkNode["id"].as(); - check.condition = checkNode["condition"].as(); + check.id = checkNode["id"].AsString(); + check.condition = checkNode["condition"].AsString(); ValidateConditionString(check.condition); - YAML::Node checkWithValidRules = YAML::Clone(checkNode); - checkWithValidRules["rules"] = YAML::Node(YAML::NodeType::Sequence); - if (checkNode["rules"]) + // create new document with valid rules + auto newDoc = checkNode.Clone(); + auto newRoot = newDoc.GetRoot(); + + // remove existing rules and create empty sequence + auto checkWithValidRules = newRoot; + newRoot.RemoveKey("rules"); + newRoot.CreateEmptySequence("rules"); + + if (checkNode.HasKey("rules")) { - for (const auto& rule : checkNode["rules"]) + const auto rules = checkNode["rules"].AsSequence(); + + for (const auto& rule : rules) { - const auto ruleStr = rule.as(); + const auto ruleStr = rule.AsString(); + if (auto ruleEvaluator = RuleEvaluatorFactory::CreateEvaluator(ruleStr)) { check.rules.push_back(std::move(ruleEvaluator)); - checkWithValidRules["rules"].push_back(ruleStr); + checkWithValidRules["rules"].AppendToSequence(ruleStr); } else { @@ -240,11 +250,11 @@ std::unique_ptr PolicyParser::ParsePolicy(nlohmann::json& policiesAn } // NOLINTNEXTLINE(misc-no-recursion) -void PolicyParser::ReplaceVariablesInNode(YAML::Node& currentNode) +void PolicyParser::ReplaceVariablesInNode(YamlNode& currentNode) { if (currentNode.IsScalar()) { - auto value = currentNode.as(); + auto value = currentNode.AsString(); for (const auto& pair : m_variablesMap) { size_t pos = 0; @@ -254,23 +264,21 @@ void PolicyParser::ReplaceVariablesInNode(YAML::Node& currentNode) pos += pair.second.length(); } } - currentNode = value; + currentNode.SetScalarValue(value); } else if (currentNode.IsMap()) { - for (auto it = currentNode.begin(); it != currentNode.end(); ++it) + for (auto& [key, node] : currentNode.AsMap()) { - ReplaceVariablesInNode(it->second); + ReplaceVariablesInNode(node); } } else if (currentNode.IsSequence()) { - // NOLINTNEXTLINE(modernize-loop-convert) - for (std::size_t i = 0; i < currentNode.size(); ++i) + auto items = currentNode.AsSequence(); + for (auto& item : items) { - YAML::Node element = currentNode[i]; - ReplaceVariablesInNode(element); - currentNode[i] = element; + ReplaceVariablesInNode(item); } } } diff --git a/src/modules/sca/src/sca_policy_parser.hpp b/src/modules/sca/src/sca_policy_parser.hpp index eb783b8cd6..22cf58a46f 100644 --- a/src/modules/sca/src/sca_policy_parser.hpp +++ b/src/modules/sca/src/sca_policy_parser.hpp @@ -2,8 +2,8 @@ #include +#include #include -#include #include #include @@ -24,9 +24,6 @@ struct StringLengthGreater /// comparator that prioritizes longer strings and, if equal in length, sorts the strings alphabetically. using PolicyVariables = std::map; -/// @brief Type alias for a function that loads a YAML file. -using LoadFileFunc = std::function; - /// @brief Parses and processes SCA policy files defined in YAML format. /// /// This class is responsible for reading an SCA policy YAML file, @@ -38,17 +35,8 @@ class PolicyParser public: /// @brief Constructs a PolicyParser and loads the YAML file. /// @param filename Path to the YAML policy file. - /// @param loadFileFunc Function to load the YAML file. - explicit PolicyParser(const std::filesystem::path& filename, LoadFileFunc loadFileFunc = {}); - - /// @brief Checks if the specified YAML file is valid. - /// - /// This function attempts to load the YAML file located at the given path. - /// If the file can be loaded without throwing an exception, it is considered valid. - /// - /// @param filename The path to the YAML file to be validated. - /// @return `true` if the file is a valid YAML file; `false` otherwise. - bool IsValidYamlFile(const std::filesystem::path& filename) const; + /// @param yamlDocument Optional pointer to an already loaded YAML document. + explicit PolicyParser(const std::filesystem::path& filename, std::unique_ptr yamlDocument = nullptr); /// @brief Parses the loaded policy file and extracts a SCAPolicy object. /// @@ -57,18 +45,15 @@ class PolicyParser /// /// @param policiesAndChecks JSON object to be filled with extracted data. /// @return A populated SCAPolicy object. - std::unique_ptr ParsePolicy(nlohmann::json& policiesAndChecks) const; + std::unique_ptr ParsePolicy(nlohmann::json& policiesAndChecks); private: /// @brief Recursively replaces variables in the YAML node with their values. - /// @param currentNode The YAML node to process. - void ReplaceVariablesInNode(YAML::Node& currentNode); - - /// @brief Root YAML node loaded from the policy file. - YAML::Node m_node; + /// @param currentNode The YamlDocument to process. + void ReplaceVariablesInNode(YamlNode& currentNode); - /// @brief Function to load the YAML file. - LoadFileFunc m_loadFileFunc; + /// @brief Document loaded from the YAML file. + std::unique_ptr m_yamlDocument; /// @brief Map of variables found in the YAML file, used for substitution. PolicyVariables m_variablesMap; From bf23261a783956728d15d2dc7563f6ad2904779d Mon Sep 17 00:00:00 2001 From: Ariel Martin Date: Mon, 7 Jul 2025 21:21:57 -0300 Subject: [PATCH 5/6] refactor: fix unit tests to use libyaml wrapper --- src/modules/sca/tests/CMakeLists.txt | 2 +- .../sca/tests/mocks/mock_yaml_document.hpp | 12 ++ .../sca/tests/sca_policy_parser_test.cpp | 109 ++++++------------ 3 files changed, 49 insertions(+), 74 deletions(-) create mode 100644 src/modules/sca/tests/mocks/mock_yaml_document.hpp diff --git a/src/modules/sca/tests/CMakeLists.txt b/src/modules/sca/tests/CMakeLists.txt index 5eb30b57c9..0f691a2b7d 100644 --- a/src/modules/sca/tests/CMakeLists.txt +++ b/src/modules/sca/tests/CMakeLists.txt @@ -78,7 +78,7 @@ add_test(NAME ProcessRuleEvaluatorTest COMMAND process_rule_evaluator_test) add_executable(sca_policy_parser_test sca_policy_parser_test.cpp) configure_target(sca_policy_parser_test) target_include_directories(sca_policy_parser_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../src) -target_link_libraries(sca_policy_parser_test PRIVATE SCA GTest::gtest GTest::gtest_main) +target_link_libraries(sca_policy_parser_test PRIVATE SCA GTest::gtest GTest::gmock GTest::gtest_main) add_test(NAME SCAPolicyParserTest COMMAND sca_policy_parser_test) if(WIN32) diff --git a/src/modules/sca/tests/mocks/mock_yaml_document.hpp b/src/modules/sca/tests/mocks/mock_yaml_document.hpp new file mode 100644 index 0000000000..89ef943526 --- /dev/null +++ b/src/modules/sca/tests/mocks/mock_yaml_document.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include + +class MockYamlDocument : public IYamlDocument +{ +public: + MOCK_METHOD(YamlNode, GetRoot, (), (override)); + MOCK_METHOD(bool, IsValidDocument, (), (const, override)); +}; diff --git a/src/modules/sca/tests/sca_policy_parser_test.cpp b/src/modules/sca/tests/sca_policy_parser_test.cpp index ad53446b89..7a9d616f1d 100644 --- a/src/modules/sca/tests/sca_policy_parser_test.cpp +++ b/src/modules/sca/tests/sca_policy_parser_test.cpp @@ -1,62 +1,39 @@ #include +#include "mocks/mock_yaml_document.hpp" #include +#include #include -// NOLINTBEGIN(bugprone-exception-escape) +using namespace testing; -namespace -{ - YAML::Node LoadFromString(const std::string& yml) - { - return YAML::Load(yml); - } -} // namespace +// NOLINTBEGIN(bugprone-exception-escape) TEST(PolicyParserTest, InvalidYamlFileNotThrows) { - const auto invalidYamlLoader = [](const std::string&) -> YAML::Node - { - throw YAML::Exception(YAML::Mark::null_mark(), "Invalid YAML"); - }; + auto mockYamlDocument = std::make_unique(); + MockYamlDocument* mockDocPtr = mockYamlDocument.get(); - EXPECT_NO_THROW({ const PolicyParser parser("dummy.yaml", invalidYamlLoader); }); + EXPECT_CALL(*mockDocPtr, IsValidDocument()).WillOnce(::testing::Return(false)); + const std::filesystem::path path("dummy.yaml"); + EXPECT_NO_THROW({ const PolicyParser parser(path, std::move(mockYamlDocument)); }); } TEST(PolicyParserTest, YamlSequenceIsValid) { - const auto sequenceYamlLoader = [](const std::string&) -> YAML::Node - { - return YAML::Load("- item1\n- item2"); - }; + auto yamlDocument = std::make_unique(std::string("- item1\n- item2")); - const PolicyParser parser("dummy.yaml", sequenceYamlLoader); - EXPECT_TRUE(parser.IsValidYamlFile("dummy.yaml")); // Sequences are valid top-level YAML + const std::filesystem::path path("dummy.yaml"); + EXPECT_NO_THROW({ const PolicyParser parser(path, std::move(yamlDocument)); }); } TEST(PolicyParserTest, YamlMapIsValid) { - const auto mapYamlLoader = [](const std::string&) -> YAML::Node - { - return YAML::Load("key: value"); - }; + auto yamlDocument = std::make_unique(std::string("key: value")); - const PolicyParser parser("dummy.yaml", mapYamlLoader); - EXPECT_TRUE(parser.IsValidYamlFile("dummy.yaml")); // Maps are valid top-level YAML -} - -TEST(PolicyParserTest, InvalidYamlStructureReturnsFalse) -{ - const auto invalidYamlLoader = [](const std::string&) -> YAML::Node - { - YAML::Node node; - node.reset(); - return node; - }; - - const PolicyParser parser("dummy.yaml", invalidYamlLoader); - EXPECT_FALSE(parser.IsValidYamlFile("dummy.yaml")); + const std::filesystem::path path("dummy.yaml"); + EXPECT_NO_THROW({ const PolicyParser parser(path, std::move(yamlDocument)); }); // Maps are valid top-level YAML } TEST(PolicyParserTest, ConstructorExtractsVariables) @@ -76,12 +53,10 @@ TEST(PolicyParserTest, ConstructorExtractsVariables) - 'f: $var11/shared exists' )"; - const auto loader = [yml](const std::string&) - { - return LoadFromString(yml); - }; + auto yamlDocument = std::make_unique(yml); - const PolicyParser parser("dummy.yaml", loader); + const std::filesystem::path path("dummy.yaml"); + PolicyParser parser(path, std::move(yamlDocument)); nlohmann::json j; const auto policyOpt = parser.ParsePolicy(j); @@ -107,12 +82,10 @@ TEST(PolicyParserTest, MissingPolicyReturnsNullopt) rules: ['f: /test exists'] )"; - const auto loader = [yml](const std::string&) - { - return LoadFromString(yml); - }; + auto yamlDocument = std::make_unique(yml); - const PolicyParser parser("dummy.yaml", loader); + const std::filesystem::path path("dummy.yaml"); + PolicyParser parser(path, std::move(yamlDocument)); nlohmann::json j; const auto policyOpt = parser.ParsePolicy(j); EXPECT_FALSE(policyOpt); @@ -127,12 +100,10 @@ TEST(PolicyParserTest, EmptyRequirementsReturnsNullopt) title: "req title" )"; - const auto loader = [yml](const std::string&) - { - return LoadFromString(yml); - }; + const std::filesystem::path path("dummy.yaml"); + auto yamlDocument = std::make_unique(yml); + PolicyParser parser(path, std::move(yamlDocument)); - const PolicyParser parser("dummy.yaml", loader); nlohmann::json j; const auto policyOpt = parser.ParsePolicy(j); EXPECT_FALSE(policyOpt); @@ -149,12 +120,10 @@ TEST(PolicyParserTest, MissingChecksReturnsNullopt) rules: ['f: /etc/passwd exists'] )"; - const auto loader = [yml](const std::string&) - { - return LoadFromString(yml); - }; + const std::filesystem::path path("dummy.yaml"); + auto yamlDocument = std::make_unique(yml); + PolicyParser parser(path, std::move(yamlDocument)); - const PolicyParser parser("dummy.yaml", loader); nlohmann::json j; const auto policyOpt = parser.ParsePolicy(j); EXPECT_FALSE(policyOpt); @@ -171,12 +140,10 @@ TEST(PolicyParserTest, InvalidConditionReturnsNullopt) rules: ['f: /etc/passwd exists'] )"; - const auto loader = [yml](const std::string&) - { - return LoadFromString(yml); - }; + const std::filesystem::path path("dummy.yaml"); + auto yamlDocument = std::make_unique(yml); + PolicyParser parser(path, std::move(yamlDocument)); - const PolicyParser parser("dummy.yaml", loader); nlohmann::json j; const auto policyOpt = parser.ParsePolicy(j); EXPECT_FALSE(policyOpt); @@ -195,12 +162,10 @@ TEST(PolicyParserTest, InvalidRuleIsHandledGracefully) - "invalid_rule" )"; - const auto loader = [yml](const std::string&) - { - return LoadFromString(yml); - }; + const std::filesystem::path path("dummy.yaml"); + auto yamlDocument = std::make_unique(yml); + PolicyParser parser(path, std::move(yamlDocument)); - const PolicyParser parser("dummy.yaml", loader); nlohmann::json j; const auto policyOpt = parser.ParsePolicy(j); @@ -232,12 +197,10 @@ TEST(PolicyParserTest, YamlNodeToJsonParsesMapWithSequenceValues) )"; - const auto loader = [yml](const std::string&) - { - return LoadFromString(yml); - }; + const std::filesystem::path path("dummy.yaml"); + auto yamlDocument = std::make_unique(yml); + PolicyParser parser(path, std::move(yamlDocument)); - const PolicyParser parser("dummy.yaml", loader); nlohmann::json j; const auto policyOpt = parser.ParsePolicy(j); From 6838ef0db0ab7b024a3bedbfabe0fcac53508cb4 Mon Sep 17 00:00:00 2001 From: Ariel Martin Date: Tue, 8 Jul 2025 16:03:31 -0300 Subject: [PATCH 6/6] feat: adds YamlNode and YamlDocument unit tests --- src/modules/sca/tests/CMakeLists.txt | 6 + src/modules/sca/tests/yaml_wrapper_test.cpp | 352 ++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 src/modules/sca/tests/yaml_wrapper_test.cpp diff --git a/src/modules/sca/tests/CMakeLists.txt b/src/modules/sca/tests/CMakeLists.txt index 0f691a2b7d..c40d8faf24 100644 --- a/src/modules/sca/tests/CMakeLists.txt +++ b/src/modules/sca/tests/CMakeLists.txt @@ -81,6 +81,12 @@ target_include_directories(sca_policy_parser_test PRIVATE ${CMAKE_CURRENT_SOURCE target_link_libraries(sca_policy_parser_test PRIVATE SCA GTest::gtest GTest::gmock GTest::gtest_main) add_test(NAME SCAPolicyParserTest COMMAND sca_policy_parser_test) +add_executable(yaml_wrapper_test yaml_wrapper_test.cpp ../src/yaml_document.cpp ../src/yaml_node.cpp) +configure_target(yaml_wrapper_test) +target_include_directories(yaml_wrapper_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../src) +target_link_libraries(yaml_wrapper_test PRIVATE GTest::gtest GTest::gtest_main yaml) +add_test(NAME YamlWrapperTest COMMAND yaml_wrapper_test) + if(WIN32) add_executable(registry_rule_evaluator_test registry_rule_evaluator_test.cpp) configure_target(registry_rule_evaluator_test) diff --git a/src/modules/sca/tests/yaml_wrapper_test.cpp b/src/modules/sca/tests/yaml_wrapper_test.cpp new file mode 100644 index 0000000000..ba63b607f7 --- /dev/null +++ b/src/modules/sca/tests/yaml_wrapper_test.cpp @@ -0,0 +1,352 @@ +#include + +#include "mocks/mock_yaml_document.hpp" +#include +#include + +#include + +using namespace testing; + +// NOLINTBEGIN(bugprone-exception-escape) + +TEST(YamlWrapperTest, YamlDocDefaultConstructor) +{ + auto doc = std::make_unique(); + + ASSERT_FALSE(doc->IsValidDocument()); +} + +TEST(YamlWrapperTest, YamlDocLoadInvalidString) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + policy: + *id: policy1 + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_FALSE(doc->IsValidDocument()); +} + +TEST(YamlWrapperTest, YamlDocLoadValidString) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + policy: + id: policy1 + checks: + - id: check1 + title: "title" + condition: "all" + rules: + - 'f: $var1/passwd exists' + - 'f: $var11/shared exists' + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); +} + +TEST(YamlWrapperTest, YamlDocGetRoot) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + policy: + id: policy1 + checks: + - id: check1 + title: "title" + condition: "all" + rules: + - 'f: $var1/passwd exists' + - 'f: $var11/shared exists' + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + EXPECT_NO_THROW({ auto node = doc->GetRoot(); }); +} + +TEST(YamlWrapperTest, YamlNodeSubscriptOperatorAndGetNodeType) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + policy: + id: policy1 + checks: + - id: check1 + title: "the_title" + condition: "all" + rules: + - 'f: $var1/passwd exists' + - 'f: $var11/shared exists' + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + ASSERT_TRUE(root.GetNodeType() == YamlNode::Type::Mapping); + EXPECT_EQ(root.GetNodeTypeAsString(), "Mapping"); + + // root node is a mapping, should take a key as subscript + auto checks = root["checks"]; + ASSERT_TRUE(checks.GetNodeType() == YamlNode::Type::Sequence); + EXPECT_EQ(checks.GetNodeTypeAsString(), "Sequence"); + + // checks node is a sequence, should take an index as subscript + // Element 0 is a mapping, should take a key as subscript + auto tittle = checks[0]["title"]; + ASSERT_TRUE(tittle.GetNodeType() == YamlNode::Type::Scalar); + EXPECT_EQ(tittle.GetNodeTypeAsString(), "Scalar"); + + auto rules = checks[0]["rules"]; + ASSERT_TRUE(rules.GetNodeType() == YamlNode::Type::Sequence); + + // checks node is a sequence, should not take a key as subscript, should throw + EXPECT_THROW({ auto fail = checks["rules"]; }, std::runtime_error); + + // root node is a mapping, should not take an index as subscript, should throw + EXPECT_THROW({ auto fail2 = root[0]; }, std::runtime_error); +} + +TEST(YamlWrapperTest, YamlNodeIs_Scalar_Sequence_Map) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + checks: + - id: check1 + title: "the_title" + condition: "all" + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + ASSERT_TRUE(root.IsMap()); + + auto checks = root["checks"]; + ASSERT_TRUE(checks.IsSequence()); + + auto tittle = checks[0]["title"]; + ASSERT_TRUE(tittle.IsScalar()); +} + +TEST(YamlWrapperTest, YamlNodeHasKey) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + checks: + - id: check1 + title: "the_title" + condition: "all" + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + ASSERT_TRUE(root.HasKey("checks")); + ASSERT_TRUE(root.HasKey("variables")); + ASSERT_FALSE(root.HasKey("wazuh")); +} + +TEST(YamlWrapperTest, YamlNodeAsString) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + + EXPECT_EQ(root["variables"]["$var1"].AsString(), "/etc"); + EXPECT_EQ(root["variables"]["$var11"].AsString(), "/usr"); +} + +TEST(YamlWrapperTest, YamlNodeAsMap) +{ + const std::string yml = R"( + mapElem1: "MyElement1" + mapElem2: "MyElement2" + mapElem3: "MyElement3" + mapElem4: "MyElement4" + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + auto rootMap = root.AsMap(); + EXPECT_EQ(rootMap["mapElem1"].AsString(), "MyElement1"); + EXPECT_EQ(rootMap["mapElem2"].AsString(), "MyElement2"); + EXPECT_EQ(rootMap["mapElem3"].AsString(), "MyElement3"); + EXPECT_EQ(rootMap["mapElem4"].AsString(), "MyElement4"); +} + +TEST(YamlWrapperTest, YamlNodeRemoveKey) +{ + const std::string yml = R"( + mapElem1: "MyElement1" + mapElem2: "MyElement2" + mapElem3: "MyElement3" + mapElem4: "MyElement4" + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + + root.RemoveKey("mapElem3"); + EXPECT_FALSE(root.HasKey("mapElem3")); +} + +TEST(YamlWrapperTest, YamlNodeAsSequence) +{ + const std::string yml = R"( + - "MySeqElement1" + - "MySeqElement2" + - "MySeqElement3" + - "MySeqElement4" + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + auto rootSeq = root.AsSequence(); + EXPECT_EQ(rootSeq[0].AsString(), "MySeqElement1"); + EXPECT_EQ(rootSeq[1].AsString(), "MySeqElement2"); + EXPECT_EQ(rootSeq[2].AsString(), "MySeqElement3"); + EXPECT_EQ(rootSeq[3].AsString(), "MySeqElement4"); +} + +TEST(YamlWrapperTest, YamlNodeAppendToSequence) +{ + const std::string yml = R"( + - "MySeqElement1" + - "MySeqElement2" + - "MySeqElement3" + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + root.AppendToSequence("MySeqElement4"); + + auto rootSeq = root.AsSequence(); + EXPECT_EQ(rootSeq[0].AsString(), "MySeqElement1"); + EXPECT_EQ(rootSeq[1].AsString(), "MySeqElement2"); + EXPECT_EQ(rootSeq[2].AsString(), "MySeqElement3"); + EXPECT_EQ(rootSeq[3].AsString(), "MySeqElement4"); +} + +TEST(YamlWrapperTest, YamlNodeSetScalarValue) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + + EXPECT_EQ(root["variables"]["$var1"].AsString(), "/etc"); + root["variables"]["$var1"].SetScalarValue("NewValue"); + EXPECT_EQ(root["variables"]["$var1"].AsString(), "NewValue"); +} + +TEST(YamlWrapperTest, YamlNodeSequenceCreation) +{ + const std::string yml = R"( + variables: + $var1: /etc + $var11: /usr + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + + root.CreateEmptySequence("NewSequence"); + + // The new sequence exists + EXPECT_TRUE(root.HasKey("NewSequence")); + + auto newSeq = root["NewSequence"]; + // The new sequence size is 0 + EXPECT_EQ(newSeq.AsSequence().size(), 0); + + newSeq.AppendToSequence("MySeqElement1"); + newSeq.AppendToSequence("MySeqElement2"); + newSeq.AppendToSequence("MySeqElement3"); + newSeq.AppendToSequence("MySeqElement4"); + EXPECT_EQ(newSeq.AsSequence().size(), 4); +} + +TEST(YamlWrapperTest, YamlNodeClone) +{ + + const std::string yml = R"( + mapElem1: "MyElement1" + mapElem2: "MyElement2" + mapElem3: "MyElement3" + mapElem4: "MyElement4" + )"; + + auto doc = std::make_unique(std::string(yml)); + + ASSERT_TRUE(doc->IsValidDocument()); + + auto root = doc->GetRoot(); + + auto newDoc = root.Clone(); + doc.reset(); + + auto newRoot = newDoc.GetRoot(); + auto newRootMap = newRoot.AsMap(); + EXPECT_EQ(newRootMap["mapElem1"].AsString(), "MyElement1"); + EXPECT_EQ(newRootMap["mapElem2"].AsString(), "MyElement2"); + EXPECT_EQ(newRootMap["mapElem3"].AsString(), "MyElement3"); + EXPECT_EQ(newRootMap["mapElem4"].AsString(), "MyElement4"); +} + +// NOLINTEND(bugprone-exception-escape)