From 9b2eb7ec17811459057f05c147213df751415e6b Mon Sep 17 00:00:00 2001 From: Robert Wojciechowski Date: Thu, 17 Apr 2025 15:49:17 +0200 Subject: [PATCH] Introduce compliance engine assessor tool --- .pre-commit-config.yaml | 8 +- src/CMakeLists.txt | 1 + .../complianceengine/src/CMakeLists.txt | 4 + .../src/assessor/BenchmarkFormatter.cpp | 28 ++ .../src/assessor/BenchmarkFormatter.hpp | 33 ++ .../src/assessor/CMakeLists.txt | 42 ++ .../src/assessor/CompactListFormatter.cpp | 41 ++ .../src/assessor/CompactListFormatter.hpp | 22 + .../src/assessor/DebugFormatter.cpp | 45 ++ .../src/assessor/DebugFormatter.hpp | 23 + .../src/assessor/JsonFormatter.cpp | 188 ++++++++ .../src/assessor/JsonFormatter.hpp | 26 ++ .../complianceengine/src/assessor/Main.cpp | 404 ++++++++++++++++++ .../complianceengine/src/assessor/Mof.cpp | 172 ++++++++ .../complianceengine/src/assessor/Mof.hpp | 36 ++ .../src/assessor/NestedListFormatter.cpp | 50 +++ .../src/assessor/NestedListFormatter.hpp | 24 ++ 17 files changed, 1145 insertions(+), 2 deletions(-) create mode 100644 src/modules/complianceengine/src/assessor/BenchmarkFormatter.cpp create mode 100644 src/modules/complianceengine/src/assessor/BenchmarkFormatter.hpp create mode 100644 src/modules/complianceengine/src/assessor/CMakeLists.txt create mode 100644 src/modules/complianceengine/src/assessor/CompactListFormatter.cpp create mode 100644 src/modules/complianceengine/src/assessor/CompactListFormatter.hpp create mode 100644 src/modules/complianceengine/src/assessor/DebugFormatter.cpp create mode 100644 src/modules/complianceengine/src/assessor/DebugFormatter.hpp create mode 100644 src/modules/complianceengine/src/assessor/JsonFormatter.cpp create mode 100644 src/modules/complianceengine/src/assessor/JsonFormatter.hpp create mode 100644 src/modules/complianceengine/src/assessor/Main.cpp create mode 100644 src/modules/complianceengine/src/assessor/Mof.cpp create mode 100644 src/modules/complianceengine/src/assessor/Mof.hpp create mode 100644 src/modules/complianceengine/src/assessor/NestedListFormatter.cpp create mode 100644 src/modules/complianceengine/src/assessor/NestedListFormatter.hpp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1c9714992..6606ae37d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,9 @@ repos: files: | (?x)( ^src/modules/complianceengine/src/.*\.h$| - ^src/modules/complianceengine/src/.*\.cpp$ + ^src/modules/complianceengine/src/.*\.cpp$| + ^src/compliance-engine-assessor/.*\.hpp$| + ^src/compliance-engine-assessor/.*\.cpp$ ) - repo: local hooks: @@ -67,7 +69,9 @@ repos: files: | (?x)( ^src/modules/complianceengine/.*\.h$| - ^src/modules/complianceengine/.*\.cpp$ + ^src/modules/complianceengine/.*\.cpp$| + ^src/compliance-engine-assessor/.*\.hpp$| + ^src/compliance-engine-assessor/.*\.cpp$ ) exclude: | (?x)( diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c426afc1a0..8255395ac7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,6 +34,7 @@ option(BUILD_MODULETEST "Build the moduletest tool" ON) option(BUILD_SAMPLES "Build samples" OFF) option(COVERAGE "Enable code coverage" OFF) option(BUILD_FUZZER "Build fuzzer" OFF) +option(BUILD_COMPLIANCE_ENGINE_ASSESSOR "Build the compliance engine assessor tool" ON) add_compile_options("-Wno-psabi;-fPIC") if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang") diff --git a/src/modules/complianceengine/src/CMakeLists.txt b/src/modules/complianceengine/src/CMakeLists.txt index ac35cd250e..767fb3c019 100644 --- a/src/modules/complianceengine/src/CMakeLists.txt +++ b/src/modules/complianceengine/src/CMakeLists.txt @@ -3,3 +3,7 @@ add_subdirectory(lib) add_subdirectory(so) + +if (BUILD_COMPLIANCE_ENGINE_ASSESSOR) + add_subdirectory(assessor) +endif() diff --git a/src/modules/complianceengine/src/assessor/BenchmarkFormatter.cpp b/src/modules/complianceengine/src/assessor/BenchmarkFormatter.cpp new file mode 100644 index 0000000000..729c237153 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/BenchmarkFormatter.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +using std::string; +using std::chrono::system_clock; + +string BenchmarkFormatter::ToISODatetime(const system_clock::time_point& tp) +{ + const auto time = system_clock::to_time_t(tp); + const auto tm = *std::gmtime(&time); // Convert to UTC time + + char buffer[32]; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &tm); + return buffer; +} + +BenchmarkFormatter::BenchmarkFormatter() +{ + mBegin = std::chrono::steady_clock::now(); +} +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/BenchmarkFormatter.hpp b/src/modules/complianceengine/src/assessor/BenchmarkFormatter.hpp new file mode 100644 index 0000000000..248b4a8879 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/BenchmarkFormatter.hpp @@ -0,0 +1,33 @@ +#ifndef COMPLIANCE_ENGINE_BENCHMARK_FORMATTER_HPP +#define COMPLIANCE_ENGINE_BENCHMARK_FORMATTER_HPP + +#include +#include +#include +#include +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +struct BenchmarkFormatter +{ + static std::string ToISODatetime(const std::chrono::system_clock::time_point& tp); + std::chrono::time_point mBegin; + + BenchmarkFormatter(); + virtual ~BenchmarkFormatter() = default; + BenchmarkFormatter(const BenchmarkFormatter&) = default; + BenchmarkFormatter& operator=(const BenchmarkFormatter&) = default; + BenchmarkFormatter(BenchmarkFormatter&&) = default; + BenchmarkFormatter& operator=(BenchmarkFormatter&&) = default; + + virtual Optional Begin(Action action) = 0; + virtual Optional AddEntry(const MOF::Resource& entry, Status status, const std::string& payload) = 0; + virtual Result Finish(Status status) = 0; +}; +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine +#endif // COMPLIANCE_ENGINE_BENCHMARK_FORMATTER_HPP diff --git a/src/modules/complianceengine/src/assessor/CMakeLists.txt b/src/modules/complianceengine/src/assessor/CMakeLists.txt new file mode 100644 index 0000000000..d9ba977ef7 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/CMakeLists.txt @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +add_compile_options("-Wall;-Wextra;-Wunused;-Werror;-Wformat;-Wformat-security;-Wno-unused-result") +set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_MODULE_PATH};${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.4.7) + message(FATAL_ERROR "gcc-4.4.7 or newer is needed") +endif() + +SET(CMAKE_CONFIGURATION_TYPES ${CMAKE_BUILD_TYPE} CACHE STRING "" FORCE) + +project(compliance-engine-assessor) +set(target_name compliance-engine-assessor) + +set(SOURCES + Main.cpp + Mof.cpp + BenchmarkFormatter.cpp + CompactListFormatter.cpp + DebugFormatter.cpp + JsonFormatter.cpp + NestedListFormatter.cpp +) + +add_executable(${target_name} ${SOURCES}) + +target_include_directories(${target_name} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${MODULES_INC_DIR} + ) + +target_link_libraries(${target_name} + ${CMAKE_DL_LIBS} + logging + commonutils + parsonlib + complianceenginelib + ) + +include(GNUInstallDirs) +install(TARGETS ${target_name} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/modules/complianceengine/src/assessor/CompactListFormatter.cpp b/src/modules/complianceengine/src/assessor/CompactListFormatter.cpp new file mode 100644 index 0000000000..f590657843 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/CompactListFormatter.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +using std::string; +using std::chrono::duration_cast; +using std::chrono::milliseconds; +using std::chrono::steady_clock; +using std::chrono::system_clock; + +Optional CompactListFormatter::Begin(const Action action) +{ + mOutput << "Action: " << (action == Action::Audit ? "Audit" : "Remediation") << "\n"; + mOutput << "OsConfig Version: " << OSCONFIG_VERSION << "\n"; + mOutput << "Timestamp: " << ToISODatetime(system_clock::now()) << "\n"; + mOutput << "Rules:\n"; + return Optional(); +} + +Optional CompactListFormatter::AddEntry(const MOF::Resource& entry, const Status status, const string& payload) +{ + mOutput << entry.resourceID << ":\n"; + mOutput << payload; + mOutput << "Status: " << (status == Status::Compliant ? "Compliant" : "NonCompliant") << "\n"; + return Optional(); +} + +Result CompactListFormatter::Finish(const Status status) +{ + mOutput << "Duration: " << std::chrono::duration_cast(steady_clock::now() - mBegin).count() << " ms\n"; + mOutput << "Status: " << (status == Status::Compliant ? "Compliant" : "NonCompliant") << "\n"; + mOutput << "End of Report"; + return mOutput.str(); +} +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/CompactListFormatter.hpp b/src/modules/complianceengine/src/assessor/CompactListFormatter.hpp new file mode 100644 index 0000000000..b80d8e78a4 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/CompactListFormatter.hpp @@ -0,0 +1,22 @@ +#ifndef COMPLIANCE_ENGINE_COMPACT_LIST_FORMATTER_HPP +#define COMPLIANCE_ENGINE_COMPACT_LIST_FORMATTER_HPP + +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +struct CompactListFormatter : public BenchmarkFormatter +{ + Optional Begin(Action action) override; + Optional AddEntry(const MOF::Resource& entry, Status status, const std::string& payload) override; + Result Finish(Status status) override; + +private: + std::ostringstream mOutput; +}; +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine +#endif // COMPLIANCE_ENGINE_COMPACT_LIST_FORMATTER_HPP diff --git a/src/modules/complianceengine/src/assessor/DebugFormatter.cpp b/src/modules/complianceengine/src/assessor/DebugFormatter.cpp new file mode 100644 index 0000000000..51ae3a35a6 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/DebugFormatter.cpp @@ -0,0 +1,45 @@ +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +using ComplianceEngine::Action; +using ComplianceEngine::Error; +using ComplianceEngine::Evaluator; +using ComplianceEngine::Optional; +using ComplianceEngine::Result; +using ComplianceEngine::Status; +using std::string; +using std::chrono::duration_cast; +using std::chrono::milliseconds; +using std::chrono::steady_clock; +using std::chrono::system_clock; + +Optional DebugFormatter::Begin(const Action action) +{ + mOutput << "Action: " << (action == Action::Audit ? "Audit" : "Remediation") << "\n"; + mOutput << "OsConfig Version: " << OSCONFIG_VERSION << "\n"; + mOutput << "Timestamp: " << ToISODatetime(system_clock::now()) << "\n"; + mOutput << "Rules:\n"; + return Optional(); +} + +Optional DebugFormatter::AddEntry(const MOF::Resource& entry, const Status status, const string& payload) +{ + mOutput << entry.resourceID << ":\n"; + mOutput << payload << "\n"; + mOutput << "Status: " << (status == Status::Compliant ? "Compliant" : "NonCompliant") << "\n"; + return Optional(); +} + +Result DebugFormatter::Finish(const Status status) +{ + mOutput << "Duration: " << std::chrono::duration_cast(steady_clock::now() - mBegin).count() << " ms\n"; + mOutput << "Status: " << (status == Status::Compliant ? "Compliant" : "NonCompliant") << "\n"; + mOutput << "End of Report"; + return mOutput.str(); +} +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/DebugFormatter.hpp b/src/modules/complianceengine/src/assessor/DebugFormatter.hpp new file mode 100644 index 0000000000..f98396f5e5 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/DebugFormatter.hpp @@ -0,0 +1,23 @@ +#ifndef COMPLIANCE_ENGINE_MMI_FORMATTER_HPP +#define COMPLIANCE_ENGINE_MMI_FORMATTER_HPP + +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +struct DebugFormatter : public BenchmarkFormatter +{ + Optional Begin(Action action) override; + Optional AddEntry(const MOF::Resource& entry, Status status, const std::string& payload) override; + Result Finish(Status status) override; + +private: + std::ostringstream mOutput; +}; +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine + +#endif // COMPLIANCE_ENGINE_MMI_FORMATTER_HPP diff --git a/src/modules/complianceengine/src/assessor/JsonFormatter.cpp b/src/modules/complianceengine/src/assessor/JsonFormatter.cpp new file mode 100644 index 0000000000..b1e58042b8 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/JsonFormatter.cpp @@ -0,0 +1,188 @@ +#include +#include +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +using std::string; +using std::chrono::duration_cast; +using std::chrono::milliseconds; +using std::chrono::steady_clock; +using std::chrono::system_clock; + +namespace +{ +Result GetModuleInfo() +{ + auto* value = json_parse_string(Engine::GetModuleInfo()); + if (nullptr == value) + { + return Error("Failed to parse JSON string", EINVAL); + } + + return JsonWrapper(value, JsonWrapperDeleter()); +} +} // anonymous namespace + +Optional JsonFormatter::Begin(const Action action) +{ + auto* value = json_value_init_object(); + if (nullptr == value) + { + return Error("Failed to initialize JSON object", ENOMEM); + } + + mJson = JsonWrapper(value, JsonWrapperDeleter()); + auto* object = json_value_get_object(mJson.get()); + if (nullptr == object) + { + return Error("Failed to get JSON object", ENOMEM); + } + + mBegin = std::chrono::steady_clock::now(); + + if (JSONSuccess != json_object_set_string(object, "osconfigVersion", OSCONFIG_VERSION)) + { + return Error("Failed to set OsConfig version", ENOMEM); + } + + auto moduleInfo = GetModuleInfo(); + if (!moduleInfo.HasValue()) + { + return moduleInfo.Error(); + } + + if (JSONSuccess != json_object_set_value(object, "module", moduleInfo.Value().release())) + { + return Error("Failed to set module info", ENOMEM); + } + + if (JSONSuccess != json_object_set_string(object, "timestamp", ToISODatetime(system_clock::now()).c_str())) + { + return Error("Failed to set timestamp", ENOMEM); + } + + if (JSONSuccess != json_object_set_string(object, "action", action == Action::Audit ? "Audit" : "Remediation")) + { + return Error("Failed to set action", ENOMEM); + } + + value = json_value_init_array(); + if (nullptr == value) + { + return Error("Failed to initialize JSON array", ENOMEM); + } + if (JSONSuccess != json_object_set_value(object, "rules", value)) + { + json_value_free(value); + return Error("Failed to set rules", ENOMEM); + } + + return Optional(); +} + +Optional JsonFormatter::AddEntry(const MOF::Resource& entry, const Status status, const string& payload) +{ + auto* value = json_value_init_object(); + if (nullptr == value) + { + return Error("Failed to initialize JSON object", ENOMEM); + } + auto result = JsonWrapper(value, ComplianceEngine::JsonWrapperDeleter()); + auto* object = json_value_get_object(result.get()); + if (nullptr == object) + { + return Error("Failed to get JSON object", ENOMEM); + } + + value = json_parse_string(payload.c_str()); + if (nullptr == value) + { + return Error("Failed to parse JSON payload", ENOMEM); + } + if (json_value_get_type(value) != JSONArray) + { + json_value_free(value); + return Error("Invalid JSON payload", EINVAL); + } + + if (JSONSuccess != json_object_set_value(object, "indicators", value)) + { + json_value_free(value); + return Error("Failed to set JSON payload", ENOMEM); + } + + if (JSONSuccess != json_object_set_string(object, "resourceID", entry.resourceID.c_str())) + { + return Error("Failed to set JSON resourceID", ENOMEM); + } + + if (JSONSuccess != json_object_set_string(object, "section", entry.benchmarkInfo.section.c_str())) + { + return Error("Failed to set JSON payloadKey", ENOMEM); + } + + if (JSONSuccess != json_object_set_string(object, "ruleName", entry.ruleName.c_str())) + { + return Error("Failed to set JSON ruleName", ENOMEM); + } + + if (JSONSuccess != json_object_set_string(object, "status", status == ComplianceEngine::Status::Compliant ? "Compliant" : "NonCompliant")) + { + return Error("Failed to set JSON status", ENOMEM); + } + + object = json_value_get_object(mJson.get()); + if (nullptr == object) + { + return Error("Failed to get JSON object", ENOMEM); + } + auto* array = json_object_get_array(object, "rules"); + if (nullptr == array) + { + return Error("Failed to get JSON array", ENOMEM); + } + + if (JSONSuccess != json_array_append_value(array, result.release())) + { + return Error("Failed to append JSON value", ENOMEM); + } + + return Optional(); +} + +Result JsonFormatter::Finish(ComplianceEngine::Status status) +{ + auto* object = json_value_get_object(mJson.get()); + if (nullptr == object) + { + return Error("Failed to get JSON object", ENOMEM); + } + + if (JSONSuccess != json_object_set_number(object, "durationMs", + std::chrono::duration_cast(std::chrono::steady_clock::now() - mBegin).count())) + { + return Error("Failed to set JSON duration", ENOMEM); + } + + if (JSONSuccess != json_object_set_string(object, "status", status == ComplianceEngine::Status::Compliant ? "Compliant" : "NonCompliant")) + { + return Error("Failed to set JSON status", ENOMEM); + } + + auto* serializedString = json_serialize_to_string_pretty(mJson.get()); + if (nullptr == serializedString) + { + return Error("Failed to serialize JSON string", ENOMEM); + } + + string result(serializedString); + json_free_serialized_string(serializedString); + + return result; +} +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/JsonFormatter.hpp b/src/modules/complianceengine/src/assessor/JsonFormatter.hpp new file mode 100644 index 0000000000..dfd5263f9d --- /dev/null +++ b/src/modules/complianceengine/src/assessor/JsonFormatter.hpp @@ -0,0 +1,26 @@ +#ifndef COMPLIANCE_ENGINE_JSON_FORMATTER_HPP +#define COMPLIANCE_ENGINE_JSON_FORMATTER_HPP + +#include +#include +#include + +struct json_object_t; + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +struct JsonFormatter : public BenchmarkFormatter +{ + Optional Begin(Action action) override; + Optional AddEntry(const MOF::Resource& entry, Status status, const std::string& payload) override; + Result Finish(Status status) override; + +private: + JsonWrapper mJson; +}; +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine + +#endif // COMPLIANCE_ENGINE_JSON_FORMATTER_HPP diff --git a/src/modules/complianceengine/src/assessor/Main.cpp b/src/modules/complianceengine/src/assessor/Main.cpp new file mode 100644 index 0000000000..b043f27927 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/Main.cpp @@ -0,0 +1,404 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using ComplianceEngine::Action; +using ComplianceEngine::CommonContext; +using ComplianceEngine::Engine; +using ComplianceEngine::Error; +using ComplianceEngine::Optional; +using ComplianceEngine::PayloadFormatter; +using ComplianceEngine::Result; +using ComplianceEngine::Status; +using ComplianceEngine::BenchmarkFormatters::BenchmarkFormatter; +using ComplianceEngine::BenchmarkFormatters::CompactListFormatter; +using ComplianceEngine::BenchmarkFormatters::DebugFormatter; +using ComplianceEngine::BenchmarkFormatters::JsonFormatter; +using ComplianceEngine::BenchmarkFormatters::NestedListFormatter; +using ComplianceEngine::MOF::Resource; +using std::ifstream; +using std::istream; +using std::string; + +namespace +{ +enum class Command +{ + Help, + Version, + Audit, + Remediate +}; + +enum class Format +{ + NestedList, + CompactList, + Json, + Debug +}; + +struct Options +{ + bool verbose = false; + bool debug = false; + Optional logFile; + Optional format; + Command command = Command::Help; + std::string input; + Optional section; +}; + +void PrintHelp(const std::string& programName) +{ + std::cout << "Usage: " + programName + "\n\n"; + std::cout << "Available optinos:\n"; + std::cout << "\t-h, --help\tShow help and exit.\n"; + std::cout << "\t-V, --version\tShow software version and exit.\n"; + std::cout << "\t-v, --verbose\tRun in verbose mode.\n"; + std::cout << "\t-d, --debug\tRun in debug mode.\n"; + std::cout << "\t-l, --log-file\tSpecify a log file. Default: print log entries to standard output.\n"; + std::cout << "\t-s, --section\tProcess only specific sections. Default: process all available rules.\n"; + std::cout << "\n"; + std::cout << "Positional arguments:\n"; + std::cout << "\tcommand\t\tDetermine whether to run in audit or remediation mode. Allowed values: {audit|remediate}.\n"; + std::cout << "\tfilename\tProcess the specified MOF file. Optional: if skipped or the value is -, the program reads standard input\n"; +} + +// Command line parser using getopt_long +Result ParseCommandLine(const int argc, char* argv[]) +{ + const auto* short_opts = "hVvdlsf"; + const option long_opts[] = {{"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"verbose", no_argument, nullptr, 'v'}, + {"debug", no_argument, nullptr, 'd'}, {"log-file", required_argument, nullptr, 'l'}, {"section", required_argument, nullptr, 's'}, + {"format", required_argument, nullptr, 'f'}, {nullptr, 0, nullptr, 0}}; + + auto result = Options{}; + int opt = getopt_long(argc, argv, short_opts, long_opts, nullptr); + while (opt != -1) + { + switch (opt) + { + case 'h': + result.command = Command::Help; + return result; + case 'V': + result.command = Command::Version; + return result; + case 'v': + result.verbose = true; + break; + case 'd': + result.debug = true; + break; + case 'l': + result.logFile = std::string(optarg); + break; + case 's': + result.section = std::string(optarg); + break; + case 'f': { + auto formatArg = std::string(optarg); + std::transform(formatArg.begin(), formatArg.end(), formatArg.begin(), ::tolower); + if (formatArg == "nested-list") + { + result.format = Format::NestedList; + } + else if (formatArg == "compact-list") + { + result.format = Format::CompactList; + } + else if (formatArg == "json") + { + result.format = Format::Json; + } + else if (formatArg == "debug") + { + result.format = Format::Debug; + } + else + { + return Error("Invalid format: " + formatArg); + } + break; + } + default: + return Error("Unknown option."); + } + + opt = getopt_long(argc, argv, short_opts, long_opts, nullptr); + } + + // After options, parse the positional arguments + if (optind < argc) + { + const std::string arg = argv[optind]; + if (arg == "audit") + { + result.command = Command::Audit; + } + else if (arg == "remediate") + { + result.command = Command::Remediate; + } + else + { + return Error("Invalid command: '" + arg + "'. Must be 'audit' or 'remediate'."); + } + ++optind; + } + else + { + return Error("Missing required command: 'audit' or 'remediate'."); + } + + // Input filename + if (optind < argc) + { + const std::string arg = argv[optind]; + result.input = arg; + ++optind; + } + + // End of positional arguments + if (optind < argc) + { + return Error("Too many arguments provided."); + } + + return result; +} +} // anonymous namespace + +int main(int argc, char* argv[]) +{ + const auto optionsResult = ParseCommandLine(argc, argv); + if (!optionsResult.HasValue()) + { + std::cerr << "Error: " << optionsResult.Error().message << std::endl; + PrintHelp(argv[0]); + return 1; + } + + const auto& options = optionsResult.Value(); + if (Command::Help == options.command) + { + PrintHelp(argv[0]); + return 0; + } + + if (Command::Version == options.command) + { + std::cout << "Compliance Engine Assessor\nVersion: " << OSCONFIG_VERSION << "\n"; + return 0; + } + + std::cerr << "Compliance Engine Assessor\n"; + std::unique_ptr benchmarkFormatter; + std::unique_ptr payloadFormatter; + if (options.format.HasValue()) + { + switch (options.format.Value()) + { + case Format::NestedList: + benchmarkFormatter = std::unique_ptr(new NestedListFormatter()); + payloadFormatter = std::unique_ptr(new ComplianceEngine::NestedListFormatter()); + break; + case Format::CompactList: + benchmarkFormatter = std::unique_ptr(new CompactListFormatter()); + payloadFormatter = std::unique_ptr(new ComplianceEngine::CompactListFormatter()); + break; + case Format::Json: + benchmarkFormatter = std::unique_ptr(new JsonFormatter()); + payloadFormatter = std::unique_ptr(new ComplianceEngine::JsonFormatter()); + break; + case Format::Debug: + benchmarkFormatter = std::unique_ptr(new DebugFormatter()); + payloadFormatter = std::unique_ptr(new ComplianceEngine::DebugFormatter()); + break; + default: + std::cerr << "Invalid format specified.\n"; + return 1; + } + } + if (!payloadFormatter) + { + payloadFormatter = std::unique_ptr(new ComplianceEngine::JsonFormatter()); + } + if (!benchmarkFormatter) + { + benchmarkFormatter = std::unique_ptr(new JsonFormatter()); + } + + auto logHandle = options.logFile.HasValue() ? OpenLog(options.logFile->c_str(), nullptr) : nullptr; + if (nullptr != logHandle) + { + SetConsoleLoggingEnabled(false); + } + + if (options.verbose) + { + SetLoggingLevel(LoggingLevel::LoggingLevelInformational); + OsConfigLogInfo(logHandle, "Verbose logging enabled"); + } + + if (options.debug) + { + SetLoggingLevel(LoggingLevel::LoggingLevelDebug); + OsConfigLogInfo(logHandle, "Debug logging enabled"); + } + + auto context = std::unique_ptr(new CommonContext(logHandle)); + Engine engine(std::move(context), std::move(payloadFormatter)); + + auto error = benchmarkFormatter->Begin(options.command == Command::Audit ? Action::Audit : Action::Remediate); + if (error) + { + OsConfigLogError(logHandle, "Failed to begin formatted output: %s", error.Value().message.c_str()); + CloseLog(&logHandle); + return 1; + } + + ifstream file; + if (!options.input.empty()) + { + file.open(options.input); + if (!file.is_open()) + { + OsConfigLogError(logHandle, "Failed to open input file: %s", options.input.c_str()); + CloseLog(&logHandle); + return 1; + } + } + + istream& inputStream = options.input.empty() ? std::cin : file; + string line; + auto status = Status::Compliant; + while (std::getline(inputStream, line)) + { + if (line.find("instance of OsConfigResource as") == std::string::npos) + { + continue; + } + + auto mofParsingResult = Resource::ParseSingleEntry(inputStream); + if (!mofParsingResult.HasValue()) + { + OsConfigLogError(logHandle, "Failed to parse MOF entry: %s", mofParsingResult.Error().message.c_str()); + CloseLog(&logHandle); + return 1; + } + + auto mofEntry = std::move(mofParsingResult.Value()); + if (options.section.HasValue()) + { + if (mofEntry.benchmarkInfo.section.find(options.section.Value()) != 0) + { + OsConfigLogDebug(logHandle, "Skipping entry %s as it does not match section %s", mofEntry.resourceID.c_str(), options.section.Value().c_str()); + continue; + } + } + + auto procedureResult = engine.MmiSet((string("procedure") + mofEntry.ruleName).c_str(), mofEntry.procedure); + if (!procedureResult.HasValue()) + { + OsConfigLogError(logHandle, "Failed to set procedure: %s", procedureResult.Error().message.c_str()); + status = Status::NonCompliant; + continue; + } + + switch (options.command) + { + case Command::Audit: { + if (mofEntry.hasInitAudit) + { + auto result = engine.MmiSet((string("init") + mofEntry.ruleName).c_str(), mofEntry.payload.Value()); + if (!result.HasValue()) + { + OsConfigLogError(logHandle, "Failed to init audit: %s", result.Error().message.c_str()); + status = Status::NonCompliant; + continue; + } + } + + auto ruleName = string("audit") + mofEntry.ruleName; + auto result = engine.MmiGet(ruleName.c_str()); + if (!result.HasValue()) + { + OsConfigLogError(logHandle, "Failed to perform audit: %s", result.Error().message.c_str()); + status = Status::NonCompliant; + continue; + } + + error = benchmarkFormatter->AddEntry(mofEntry, result.Value().status, result.Value().payload); + if (error) + { + OsConfigLogError(logHandle, "Failed to add entry to JSON formatter: %s", error.Value().message.c_str()); + status = Status::NonCompliant; + break; + } + + if (result.Value().status != Status::Compliant) + { + status = Status::NonCompliant; + } + + break; + } + + case Command::Remediate: { + auto ruleName = string("remediate") + mofEntry.ruleName; + auto result = engine.MmiSet(ruleName.c_str(), mofEntry.payload.Value()); + if (!result.HasValue()) + { + OsConfigLogError(logHandle, "Failed to remediate: %s", result.Error().message.c_str()); + status = Status::NonCompliant; + continue; + } + + error = benchmarkFormatter->AddEntry(mofEntry, result.Value(), "[]"); + if (error) + { + OsConfigLogError(logHandle, "Failed to add entry to JSON formatter: %s", error.Value().message.c_str()); + status = Status::NonCompliant; + continue; + } + + if (result.Value() != Status::Compliant) + { + status = Status::NonCompliant; + } + + break; + } + + default: + break; + } + } + + auto result = benchmarkFormatter->Finish(status); + CloseLog(&logHandle); + if (!result.HasValue()) + { + OsConfigLogError(logHandle, "Failed to finish formatted output: %s", result.Error().message.c_str()); + return 1; + } + + std::cout << result.Value() << "\n"; + return status == Status::Compliant ? 0 : 1; +} diff --git a/src/modules/complianceengine/src/assessor/Mof.cpp b/src/modules/complianceengine/src/assessor/Mof.cpp new file mode 100644 index 0000000000..0c3753eef7 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/Mof.cpp @@ -0,0 +1,172 @@ +#include +#include +#include + +namespace ComplianceEngine +{ +namespace MOF +{ +using std::array; +using std::istream; +using std::map; +using std::string; + +namespace +{ +string GetValue(const std::string& line) +{ + const auto start = line.find('"'); + if (start == std::string::npos) + { + return std::string(); + } + const auto end = line.find('"', start + 1); + if (end == std::string::npos) + { + return std::string(); + } + return line.substr(start + 1, end - (start + 1)); +}; +} // anonymous namespace + +Result SemVer::Parse(const std::string& version) +{ + if (version.find("v") != 0) + { + return Error("Invalid version format: must start with 'v' prefix"); + } + + const auto pos1 = version.find('.', 1); + if (pos1 == std::string::npos) + { + return Error("Invalid version format: missing minor version"); + } + + const auto pos2 = version.find('.', pos1 + 1); + if (pos2 == std::string::npos) + { + return Error("Invalid version format: missing patch version"); + } + + try + { + const auto major = std::stoi(version.substr(1, pos1)); + const auto minor = std::stoi(version.substr(pos1 + 1, pos2 - pos1 - 1)); + const auto patch = std::stoi(version.substr(pos2 + 1)); + + return SemVer{major, minor, patch}; + } + catch (std::exception& e) + { + return Error(string("Invalid version format: ") + e.what()); + } +} + +Result Resource::ParseSingleEntry(std::istream& stream) +{ + string line; + Optional resourceID; + Optional benchmarkInfo; + Optional procedure; + bool hasInitAudit = false; + Optional ruleName; + Optional payload; + while (std::getline(stream, line)) + { + if (line.find("ResourceID") != string::npos) + { + resourceID = GetValue(line); + continue; + } + + if (line.find("PayloadKey") != string::npos) + { + auto result = CISBenchmarkInfo::Parse(GetValue(line)); + if (!result.HasValue()) + { + return Error("Failed to parse PayloadKey: " + result.Error().message); + } + benchmarkInfo = std::move(result.Value()); + continue; + } + + if (line.find("ProcedureObjectValue") != std::string::npos) + { + procedure = GetValue(line); + continue; + } + + if (line.find("InitObjectName") != std::string::npos) + { + auto value = GetValue(line); + if (value.find("init") != 0) + { + return Error("Invalid init object name"); + } + hasInitAudit = true; + continue; + } + + if (line.find("ReportedObjectName") != std::string::npos) + { + auto value = GetValue(line); + if (value.find("audit") != 0) + { + return Error("Invalid reported object name"); + } + ruleName = value.substr(strlen("audit")); + continue; + } + + if (line.find("DesiredObjectValue") != std::string::npos) + { + payload = GetValue(line); + continue; + } + + if (line.find("};") != std::string::npos) + { + // End of MOF entry, validate and return + break; + } + } + + Resource resource; + if (!resourceID.HasValue()) + { + return Error("Failed to parse MOF file: ResourceID is missing"); + } + resource.resourceID = std::move(resourceID.Value()); + + if (!benchmarkInfo.HasValue()) + { + return Error("Failed to parse MOF file: PayloadKey is missing"); + } + resource.benchmarkInfo = std::move(benchmarkInfo.Value()); + auto& section = resource.benchmarkInfo.section; + std::transform(section.begin(), section.end(), section.begin(), [](char c) { + if (c == '/') + { + return '.'; + } + return c; + }); + + if (!ruleName.HasValue()) + { + return Error("Failed to parse MOF file: ReportedObjectName is missing"); + } + resource.ruleName = std::move(ruleName.Value()); + + if (!procedure.HasValue()) + { + return Error("Failed to parse MOF file: ProcedureObjectValue is missing"); + } + resource.procedure = std::move(procedure.Value()); + resource.payload = std::move(payload); + resource.hasInitAudit = hasInitAudit; + + return resource; +} +} // namespace MOF +} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/Mof.hpp b/src/modules/complianceengine/src/assessor/Mof.hpp new file mode 100644 index 0000000000..8883e01b5e --- /dev/null +++ b/src/modules/complianceengine/src/assessor/Mof.hpp @@ -0,0 +1,36 @@ +#ifndef COMPLIANCE_ENGINE_ASSESOR_MOF_HPP +#define COMPLIANCE_ENGINE_ASSESOR_MOF_HPP + +#include +#include +#include +#include +#include + +namespace ComplianceEngine +{ +namespace MOF +{ +struct SemVer +{ + int major; + int minor; + int patch; + + static Result Parse(const std::string& version); +}; + +struct Resource +{ + std::string resourceID; + CISBenchmarkInfo benchmarkInfo; + std::string procedure; + Optional payload; + std::string ruleName; + bool hasInitAudit = false; + + static Result ParseSingleEntry(std::istream& stream); +}; +} // namespace MOF +} // namespace ComplianceEngine +#endif // COMPLIANCE_ENGINE_ASSESOR_MOF_HPP diff --git a/src/modules/complianceengine/src/assessor/NestedListFormatter.cpp b/src/modules/complianceengine/src/assessor/NestedListFormatter.cpp new file mode 100644 index 0000000000..ca24f157b8 --- /dev/null +++ b/src/modules/complianceengine/src/assessor/NestedListFormatter.cpp @@ -0,0 +1,50 @@ +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +using ComplianceEngine::Action; +using ComplianceEngine::Error; +using ComplianceEngine::Evaluator; +using ComplianceEngine::Optional; +using ComplianceEngine::Result; +using ComplianceEngine::Status; +using std::string; +using std::chrono::duration_cast; +using std::chrono::milliseconds; +using std::chrono::steady_clock; +using std::chrono::system_clock; + +Optional NestedListFormatter::Begin(const Action action) +{ + mOutput << "Action: " << (action == Action::Audit ? "Audit" : "Remediation") << "\n"; + mOutput << "OsConfig Version: " << OSCONFIG_VERSION << "\n"; + mOutput << "Timestamp: " << ToISODatetime(system_clock::now()) << "\n"; + mOutput << "Rules:\n"; + return Optional(); +} + +Optional NestedListFormatter::AddEntry(const MOF::Resource& entry, const Status status, const string& payload) +{ + (void)entry; + std::string line; + std::istringstream payloadStream(payload); + while (std::getline(payloadStream, line)) + { + mOutput << " " << line << "\n"; + } + mOutput << " Status: " << (status == Status::Compliant ? "Compliant" : "NonCompliant") << "\n"; + return Optional(); +} + +Result NestedListFormatter::Finish(const Status status) +{ + mOutput << "Duration: " << std::chrono::duration_cast(steady_clock::now() - mBegin).count() << " ms\n"; + mOutput << "Status: " << (status == Status::Compliant ? "Compliant" : "NonCompliant") << "\n"; + mOutput << "End of Report"; + return mOutput.str(); +} +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/NestedListFormatter.hpp b/src/modules/complianceengine/src/assessor/NestedListFormatter.hpp new file mode 100644 index 0000000000..7978798d7b --- /dev/null +++ b/src/modules/complianceengine/src/assessor/NestedListFormatter.hpp @@ -0,0 +1,24 @@ +#ifndef COMPLIANCE_ENGINE_NESTED_LIST_FORMATTER_HPP +#define COMPLIANCE_ENGINE_NESTED_LIST_FORMATTER_HPP + +#include +#include +#include + +namespace ComplianceEngine +{ +namespace BenchmarkFormatters +{ +struct NestedListFormatter : public BenchmarkFormatter +{ + Optional Begin(Action action) override; + Optional AddEntry(const MOF::Resource& entry, Status status, const std::string& payload) override; + Result Finish(Status status) override; + +private: + std::ostringstream mOutput; +}; +} // namespace BenchmarkFormatters +} // namespace ComplianceEngine + +#endif // COMPLIANCE_ENGINE_NESTED_LIST_FORMATTER_HPP