From 4d584981933ea712324ee58a19cd903f9d9d6148 Mon Sep 17 00:00:00 2001 From: Robert Wojciechowski Date: Tue, 25 Nov 2025 14:10:58 +0100 Subject: [PATCH 1/2] Add new version of systemd parameter checks - CIS has reworked the way they match parameter values from regular expressions to matching on section, key and value with few operators --- .../complianceengine/src/lib/CMakeLists.txt | 8 +- .../complianceengine/src/lib/ProcedureMap.cpp | 22 +- .../complianceengine/src/lib/ProcedureMap.h | 49 ++- .../src/lib/SystemdCatConfig.cpp | 36 +- .../src/lib/SystemdCatConfig.h | 2 +- .../src/lib/payload.schema.json | 6 + .../lib/procedures/EnsureSystemdParameter.cpp | 327 +++++++++++++++ .../lib/procedures/EnsureSystemdParameter.h | 70 ++++ .../src/lib/procedures/SystemdConfig.cpp | 159 -------- .../src/lib/procedures/SystemdConfig.h | 29 -- .../tests/procedures/SystemdConfigTest.cpp | 377 +++++++++++++++++- 11 files changed, 875 insertions(+), 210 deletions(-) create mode 100644 src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.cpp create mode 100644 src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.h delete mode 100644 src/modules/complianceengine/src/lib/procedures/SystemdConfig.cpp delete mode 100644 src/modules/complianceengine/src/lib/procedures/SystemdConfig.h diff --git a/src/modules/complianceengine/src/lib/CMakeLists.txt b/src/modules/complianceengine/src/lib/CMakeLists.txt index 9f95044955..3cb4e6b1d6 100644 --- a/src/modules/complianceengine/src/lib/CMakeLists.txt +++ b/src/modules/complianceengine/src/lib/CMakeLists.txt @@ -10,8 +10,8 @@ endif() find_package(Lua REQUIRED) set(PROCEDURES - procedures/EnsureAccountsWithoutShellAreLocked.cpp procedures/AuditdRulesCheck.cpp + procedures/EnsureAccountsWithoutShellAreLocked.cpp procedures/EnsureAllGroupsFromEtcPasswdExistInEtcGroup.cpp procedures/EnsureApparmorProfiles.cpp procedures/EnsureDconf.cpp @@ -38,6 +38,7 @@ set(PROCEDURES procedures/EnsureSshdOption.cpp procedures/EnsureSysctl.cpp procedures/EnsureSystemAccountsDoNotHaveValidShell.cpp + procedures/EnsureSystemdParameter.cpp procedures/EnsureUserIsOnlyAccountWith.cpp procedures/EnsureWirelessIsDisabled.cpp procedures/EnsureXdmcp.cpp @@ -45,7 +46,6 @@ set(PROCEDURES procedures/FileRegexMatch.cpp procedures/PackageInstalled.cpp procedures/SCE.cpp - procedures/SystemdConfig.cpp procedures/SystemdUnitState.cpp procedures/TestingProcedures.cpp procedures/UfwStatus.cpp @@ -53,8 +53,8 @@ set(PROCEDURES procedures/EnsureSshKeyPerms.cpp ) set(SCHEMAS - procedures/EnsureAccountsWithoutShellAreLocked.schema.json procedures/AuditdRulesCheck.schema.json + procedures/EnsureAccountsWithoutShellAreLocked.schema.json procedures/EnsureAllGroupsFromEtcPasswdExistInEtcGroup.schema.json procedures/EnsureApparmorProfiles.schema.json procedures/EnsureDconf.schema.json @@ -81,6 +81,7 @@ set(SCHEMAS procedures/EnsureSshdOption.schema.json procedures/EnsureSysctl.schema.json procedures/EnsureSystemAccountsDoNotHaveValidShell.schema.json + procedures/EnsureSystemdParameter.schema.json procedures/EnsureUserIsOnlyAccountWith.schema.json procedures/EnsureWirelessIsDisabled.schema.json procedures/EnsureXdmcp.schema.json @@ -88,7 +89,6 @@ set(SCHEMAS procedures/FileRegexMatch.schema.json procedures/PackageInstalled.schema.json procedures/SCE.schema.json - procedures/SystemdConfig.schema.json procedures/SystemdUnitState.schema.json procedures/TestingProcedures.schema.json procedures/UfwStatus.schema.json diff --git a/src/modules/complianceengine/src/lib/ProcedureMap.cpp b/src/modules/complianceengine/src/lib/ProcedureMap.cpp index 2d664b6d9b..ae4abb0569 100644 --- a/src/modules/complianceengine/src/lib/ProcedureMap.cpp +++ b/src/modules/complianceengine/src/lib/ProcedureMap.cpp @@ -59,6 +59,12 @@ const char* Bindings::names[] = {"option", "value", "op" // EnsureSysctl.h:21 const char* Bindings::names[] = {"sysctlName", "value"}; +// EnsureSystemdParameter.h:25 +const char* Bindings::names[] = {"parameter", "valueRegex", "file", "dir"}; + +// EnsureSystemdParameter.h:66 +const char* Bindings::names[] = {"file", "section", "option", "expression", "value"}; + // EnsureUserIsOnlyAccountWith.h:26 const char* Bindings::names[] = {"username", "uid", "gid", "test_etcPasswdPath"}; @@ -77,9 +83,6 @@ const char* Bindings::names[] = {"packageName", "minPack // SCE.h:18 const char* Bindings::names[] = {"scriptName", "ENVIRONMENT"}; -// SystemdConfig.h:25 -const char* Bindings::names[] = {"parameter", "valueRegex", "file", "dir"}; - // SystemdUnitState.h:28 const char* Bindings::names[] = {"unitName", "ActiveState", "LoadState", "UnitFileState", "Unit"}; @@ -131,6 +134,7 @@ const ProcedureMap Evaluator::mProcedureMap = { {"EnsureSshdOption", {MakeHandler(AuditEnsureSshdOption), nullptr}}, {"EnsureSysctl", {MakeHandler(AuditEnsureSysctl), nullptr}}, {"EnsureSystemAccountsDoNotHaveValidShell", {MakeHandler(AuditEnsureSystemAccountsDoNotHaveValidShell), nullptr}}, + {"EnsureSystemdParameterV4", {MakeHandler(AuditEnsureSystemdParameterV4), nullptr}}, {"EnsureUfwOpenPorts", {MakeHandler(AuditEnsureUfwOpenPorts), nullptr}}, {"EnsureUserIsOnlyAccountWith", {MakeHandler(AuditEnsureUserIsOnlyAccountWith), nullptr}}, {"EnsureWirelessIsDisabled", {MakeHandler(AuditEnsureWirelessIsDisabled), nullptr}}, @@ -246,6 +250,18 @@ string to_string(const ComplianceEngine::EnsureSshdOptionMode value) noexcept(fa return it->second; } +string to_string(const ComplianceEngine::SystemdParameterExpression value) noexcept(false) +{ + const auto& map = ComplianceEngine::MapEnum(); + static const auto revmap = ComplianceEngine::RevertMap(map); + const auto it = revmap.find(value); + if (revmap.end() == it) + { + throw std::out_of_range("Invalid enum value"); + } + return it->second; +} + string to_string(const ComplianceEngine::RegexType value) noexcept(false) { const auto& map = ComplianceEngine::MapEnum(); diff --git a/src/modules/complianceengine/src/lib/ProcedureMap.h b/src/modules/complianceengine/src/lib/ProcedureMap.h index 7a2f6fe6fd..7aa84e59a7 100644 --- a/src/modules/complianceengine/src/lib/ProcedureMap.h +++ b/src/modules/complianceengine/src/lib/ProcedureMap.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,6 @@ #include #include #include -#include #include #include #include @@ -163,6 +163,20 @@ inline const std::map& MapEnum +inline const std::map& MapEnum() +{ + static const std::map map = { + {"lt", SystemdParameterExpression::LessThan}, + {"le", SystemdParameterExpression::LessOrEqual}, + {"gt", SystemdParameterExpression::GreaterThan}, + {"ge", SystemdParameterExpression::GreaterOrEqual}, + {"eq", SystemdParameterExpression::Equal}, + }; + return map; +} + // Maps the RegexType enum labels to the enum values. template <> inline const std::map& MapEnum() @@ -404,6 +418,26 @@ struct Bindings static constexpr auto members = std::make_tuple(&T::sysctlName, &T::value); }; +// Defines the bindings for the SystemdParameterParams structure. +template <> +struct Bindings +{ + using T = SystemdParameterParams; + static constexpr size_t size = 4; + static const char* names[]; + static constexpr auto members = std::make_tuple(&T::parameter, &T::valueRegex, &T::file, &T::dir); +}; + +// Defines the bindings for the EnsureSystemdParameterV4Params structure. +template <> +struct Bindings +{ + using T = EnsureSystemdParameterV4Params; + static constexpr size_t size = 5; + static const char* names[]; + static constexpr auto members = std::make_tuple(&T::file, &T::section, &T::option, &T::expression, &T::value); +}; + // Defines the bindings for the EnsureUserIsOnlyAccountWithParams structure. template <> struct Bindings @@ -464,16 +498,6 @@ struct Bindings static constexpr auto members = std::make_tuple(&T::scriptName, &T::ENVIRONMENT); }; -// Defines the bindings for the SystemdParameterParams structure. -template <> -struct Bindings -{ - using T = SystemdParameterParams; - static constexpr size_t size = 4; - static const char* names[]; - static constexpr auto members = std::make_tuple(&T::parameter, &T::valueRegex, &T::file, &T::dir); -}; - // Defines the bindings for the SystemdUnitStateParams structure. template <> struct Bindings @@ -552,6 +576,9 @@ string to_string(ComplianceEngine::EnsureSshdOptionOperation value) noexcept(fal // Returns a string representation of the EnsureSshdOptionMode enum value. string to_string(ComplianceEngine::EnsureSshdOptionMode value) noexcept(false); // NOLINT(*-identifier-naming) +// Returns a string representation of the SystemdParameterExpression enum value. +string to_string(ComplianceEngine::SystemdParameterExpression value) noexcept(false); // NOLINT(*-identifier-naming) + // Returns a string representation of the RegexType enum value. string to_string(ComplianceEngine::RegexType value) noexcept(false); // NOLINT(*-identifier-naming) diff --git a/src/modules/complianceengine/src/lib/SystemdCatConfig.cpp b/src/modules/complianceengine/src/lib/SystemdCatConfig.cpp index 60306594a6..16543b9a64 100644 --- a/src/modules/complianceengine/src/lib/SystemdCatConfig.cpp +++ b/src/modules/complianceengine/src/lib/SystemdCatConfig.cpp @@ -2,14 +2,46 @@ // Licensed under the MIT License. #include +#include namespace ComplianceEngine { -Result SystemdCatConfig(const std::string& filename, ContextInterface& context) +namespace { - auto result = context.ExecuteCommand("systemd-analyze cat-config " + filename); +Result DetermineCommandPath(const std::string& command, const ContextInterface& context) +{ + auto result = context.ExecuteCommand("readlink -e /bin/" + command); + if (result.HasValue()) + { + OsConfigLogInfo(context.GetLogHandle(), "'%s' path is: %s", command.c_str(), result->c_str()); + return std::move(result.Value()); + } + + result = context.ExecuteCommand("readlink -e /usr/bin/" + command); + if (result.HasValue()) + { + OsConfigLogInfo(context.GetLogHandle(), "'%s' path is: %s", command.c_str(), result->c_str()); + return std::move(result.Value()); + } + + OsConfigLogError(context.GetLogHandle(), "Failed to determine systemd-analyze command path"); + return result.Error(); +} +} // anonymous namespace + +Result SystemdCatConfig(const std::string& filename, const ContextInterface& context) +{ + static const auto commandPath = DetermineCommandPath("systemd-analyze", context); + if (!commandPath.HasValue()) + { + return commandPath.Error(); + } + + auto result = context.ExecuteCommand(commandPath.Value() + " cat-config " + filename); if (!result.HasValue()) { + OsConfigLogError(context.GetLogHandle(), "Failed to execute systemd-analyze command: %s", result.Error().message.c_str()); + OSConfigTelemetryStatusTrace("ExecuteCommand", result.Error().code); return result; } diff --git a/src/modules/complianceengine/src/lib/SystemdCatConfig.h b/src/modules/complianceengine/src/lib/SystemdCatConfig.h index de9060c0d7..7930446701 100644 --- a/src/modules/complianceengine/src/lib/SystemdCatConfig.h +++ b/src/modules/complianceengine/src/lib/SystemdCatConfig.h @@ -9,7 +9,7 @@ namespace ComplianceEngine { -Result SystemdCatConfig(const std::string& filename, ContextInterface& context); +Result SystemdCatConfig(const std::string& filename, const ContextInterface& context); } // namespace ComplianceEngine #endif // COMPLIANCEENGINE_SYSTEMD_CAT_CONFIG_H diff --git a/src/modules/complianceengine/src/lib/payload.schema.json b/src/modules/complianceengine/src/lib/payload.schema.json index 02f0b3fbbe..6756aa7a30 100644 --- a/src/modules/complianceengine/src/lib/payload.schema.json +++ b/src/modules/complianceengine/src/lib/payload.schema.json @@ -242,6 +242,9 @@ { "$ref": "procedures/EnsureSystemAccountsDoNotHaveValidShell.schema.json#/definitions/audit" }, + { + "$ref": "procedures/EnsureSystemdParameter.schema.json#/definitions/audit" + }, { "$ref": "procedures/EnsureUserIsOnlyAccountWith.schema.json#/definitions/audit" }, @@ -369,6 +372,9 @@ { "$ref": "procedures/EnsureSystemAccountsDoNotHaveValidShell.schema.json#/definitions/remediation" }, + { + "$ref": "procedures/EnsureSystemdParameter.schema.json#/definitions/remediation" + }, { "$ref": "procedures/EnsureUserIsOnlyAccountWith.schema.json#/definitions/remediation" }, diff --git a/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.cpp b/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.cpp new file mode 100644 index 0000000000..53f29bb63e --- /dev/null +++ b/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.cpp @@ -0,0 +1,327 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include +#include +#include // For std::to_string(enum) +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using ComplianceEngine::Optional; + +namespace ComplianceEngine +{ + +namespace +{ +typedef std::map> SystemdConfigMap_t; + +Result GetSystemdConfig(SystemdConfigMap_t& config, const std::string& filename, ContextInterface& context) +{ + auto result = SystemdCatConfig(filename, context); + if (!result.HasValue()) + { + OsConfigLogError(context.GetLogHandle(), "Failed to execute systemd-analyze command: %s", result.Error().message.c_str()); + return result.Error(); + } + + std::istringstream stream(result.Value()); + std::string line; + std::string currentConfig = ""; + while (std::getline(stream, line)) + { + if (line.empty()) + { + continue; + } + if ('#' == line[0]) + { + if ((line.size() > strlen("# .conf")) && ('#' == line[0]) && (".conf" == line.substr(line.size() - strlen(".conf")))) + { + currentConfig = line.substr(2); + } + continue; + } + size_t eqSign = line.find('='); + if (eqSign == std::string::npos) + { + OsConfigLogError(context.GetLogHandle(), "Invalid line in systemd config: %s", line.c_str()); + OSConfigTelemetryStatusTrace("getline", EINVAL); + continue; + } + std::string key = line.substr(0, eqSign); + std::string value = line.substr(eqSign + 1); + config[key] = std::make_pair(value, currentConfig); + } + return true; +} +} // namespace + +Result AuditSystemdParameter(const SystemdParameterParams& params, IndicatorsTree& indicators, ContextInterface& context) +{ + auto log = context.GetLogHandle(); + + if (!params.dir.HasValue() && !params.file.HasValue()) + { + OsConfigLogError(log, "Error: SystemdParameter: neither 'file' nor 'dir' argument is provided"); + OSConfigTelemetryStatusTrace("dir.empty && filename.empty", EINVAL); + return Error("Neither 'file' nor 'dir' argument is provided"); + } + if (params.dir.HasValue() && params.file.HasValue()) + { + OsConfigLogError(log, "Error: SystemdParameter: both 'file' and 'dir' arguments are provided, only one is allowed"); + OSConfigTelemetryStatusTrace("one dir or file only", EINVAL); + return Error("Both 'file' and 'dir' arguments are provided, only one is allowed"); + } + + SystemdConfigMap_t config; + if (params.file.HasValue()) + { + OsConfigLogDebug(log, "Getting systemd config for file '%s'", params.file->c_str()); + auto result = GetSystemdConfig(config, params.file.Value(), context); + if (!result.HasValue()) + { + OsConfigLogError(log, "Failed to get systemd config for file '%s' - %s", params.file->c_str(), result.Error().message.c_str()); + OSConfigTelemetryStatusTrace("GetSystemdConfig", result.Error().code); + return result.Error(); + } + } + else + { + OsConfigLogDebug(log, "Getting systemd config for directory '%s'", params.dir->c_str()); + bool anySuccess = false; + char* paths[] = {const_cast(params.dir->c_str()), nullptr}; + FTS* file_system = fts_open(paths, FTS_NOCHDIR | FTS_PHYSICAL, nullptr); + if (!file_system) + { + OsConfigLogError(log, "Failed to open directory '%s' with fts", params.dir->c_str()); + OSConfigTelemetryStatusTrace("fts_open", EINVAL); + return Error("Failed to open directory '" + params.dir.Value() + "'"); + } + + FTSENT* node = nullptr; + while ((node = fts_read(file_system)) != nullptr) + { + if (node->fts_info == FTS_F) + { + std::string filePath = node->fts_path; + if (filePath.size() >= strlen(".conf") && filePath.substr(filePath.size() - 5) == ".conf") + { + OsConfigLogDebug(log, "Getting systemd config for file '%s' in directory '%s'", filePath.c_str(), params.dir->c_str()); + auto result = GetSystemdConfig(config, filePath, context); + if (!result.HasValue()) + { + OsConfigLogError(log, "Failed to get systemd config for file '%s' - %s", filePath.c_str(), result.Error().message.c_str()); + OSConfigTelemetryStatusTrace("GetSystemdConfig", result.Error().code); + } + else + { + anySuccess = true; + OsConfigLogDebug(log, "Successfully got systemd config for file '%s'", filePath.c_str()); + } + } + } + } + fts_close(file_system); + if (!anySuccess) + { + OsConfigLogError(log, "No valid systemd config files found in directory '%s'", params.dir->c_str()); + OSConfigTelemetryStatusTrace("fts_close", EINVAL); + return Error("No valid systemd config files found in directory '" + params.dir.Value() + "'"); + } + } + + auto paramIt = config.find(params.parameter); + if (paramIt == config.end()) + { + OsConfigLogInfo(log, "Parameter '%s' not found", params.parameter.c_str()); + return indicators.NonCompliant("Parameter '" + params.parameter + "' not found"); + } + + OsConfigLogDebug(log, "Parameter '%s' found in file '%s' with value '%s'", params.parameter.c_str(), paramIt->second.second.c_str(), + paramIt->second.first.c_str()); + if (!regex_match(paramIt->second.first, params.valueRegex)) + { + OsConfigLogInfo(log, "Parameter '%s' in file '%s' does not match regex", params.parameter.c_str(), paramIt->second.second.c_str()); + return indicators.NonCompliant("Parameter '" + params.parameter + "' value '" + paramIt->second.first + "' in file '" + paramIt->second.second + + "' does not match regex"); + } + else + { + return indicators.Compliant("Parameter '" + params.parameter + "' found in file '" + paramIt->second.second + "' with value '" + + paramIt->second.first + "'"); + } +} + +namespace +{ +Result Compare(const std::string& lhs, const std::string& rhs, const SystemdParameterExpression operation, ContextInterface& context) +{ + // Other comparison operators + const auto l = TryStringToInt(lhs); + if (!l.HasValue()) + { + // Fall back to string comparison in case of equality check + if (operation == SystemdParameterExpression::Equal) + { + return lhs == rhs; + } + + OsConfigLogError(context.GetLogHandle(), "Failed to convert value '%s' to decimal", lhs.c_str()); + return l.Error(); + } + + const auto r = TryStringToInt(rhs); + if (!r.HasValue()) + { + // Fall back to string comparison in case of equality check + if (operation == SystemdParameterExpression::Equal) + { + return lhs == rhs; + } + + OsConfigLogError(context.GetLogHandle(), "Failed to convert value '%s' to decimal", rhs.c_str()); + return r.Error(); + } + + switch (operation) + { + case SystemdParameterExpression::LessThan: + return l.Value() < r.Value(); + case SystemdParameterExpression::LessOrEqual: + return l.Value() <= r.Value(); + case SystemdParameterExpression::GreaterThan: + return l.Value() > r.Value(); + case SystemdParameterExpression::GreaterOrEqual: + return l.Value() >= r.Value(); + default: + break; + } + + return false; +} +} // anonymous namespace + +Result AuditEnsureSystemdParameterV4(const EnsureSystemdParameterV4Params& params, IndicatorsTree& indicators, ContextInterface& context) +{ + const auto log = context.GetLogHandle(); + OsConfigLogDebug(log, "Searching for systemd configuration section '%s/%s' to match '%s' with expression: %s", params.section.c_str(), + params.option.c_str(), params.value.c_str(), std::to_string(params.expression).c_str()); + const auto existenceParams = AuditEnsureFileExistsParams{params.file}; + const auto existenceResult = AuditEnsureFileExists(existenceParams, indicators, context); + if (!existenceResult) + { + OsConfigLogError(log, "Failed to check file '%s' existence: %s", params.file.c_str(), existenceResult.Error().message.c_str()); + OSConfigTelemetryStatusTrace("AuditEnsureFileExists", existenceResult.Error().code); + return existenceResult.Error(); + } + + if (existenceResult.Value() != Status::Compliant) + { + return existenceResult.Value(); + } + + const auto catConfigResult = SystemdCatConfig(params.file, context); + if (!catConfigResult.HasValue()) + { + OsConfigLogError(log, "Failed to execute systemd-analyze command: %s", catConfigResult.Error().message.c_str()); + return catConfigResult.Error(); + } + + std::istringstream stream(catConfigResult.Value()); + std::string line; + Optional section; + Optional value; + while (std::getline(stream, line)) + { + line = TrimWhiteSpaces(line); + if (line.empty() || line[0] == '#') + { + continue; + } + + if (line[0] == '[') + { + const auto end = line.find(']', 1); + if (end == std::string::npos || 1 == end) + { + OsConfigLogError(log, "Failed to parse line '%s': invalid section", line.c_str()); + OSConfigTelemetryStatusTrace("systemd line parser", EINVAL); + return Error("invalid section", EINVAL); + } + + section = line.substr(1, end - 1); + OsConfigLogDebug(log, "Parsed section: '%s'", section.Value().c_str()); + continue; + } + + if (!section.HasValue()) + { + OsConfigLogDebug(log, "Ignoring line '%s': outside of a valid section", line.c_str()); + continue; + } + + if (section.Value() != params.section) + { + OsConfigLogDebug(log, "Ignoring line '%s': section mismatch", line.c_str()); + continue; + } + + const auto eqSign = line.find('='); + if (eqSign == std::string::npos) + { + OsConfigLogError(log, "Invalid line in systemd config: %s", line.c_str()); + OSConfigTelemetryStatusTrace("getline", EINVAL); + continue; + } + + const auto key = TrimWhiteSpaces(line.substr(0, eqSign)); + OsConfigLogDebug(log, "Option: %s", key.c_str()); + if (key != params.option) + { + OsConfigLogDebug(log, "Ignoring line '%s': option name mismatch", line.c_str()); + continue; + } + + // Value found, continue searching and compare after all occurences have been identified. + // We intentionally override the value if it already exists, as the last printed value is + // used by systemd. + value = TrimWhiteSpaces(line.substr(eqSign + 1)); + if (value->size() >= 2 && value.Value()[0] == '"' && value.Value()[value->size() - 1] == '"') + { + OsConfigLogDebug(log, "Extracting quoted value from '%s'", value->c_str()); + value = value->substr(1, value->size() - 2); + } + OsConfigLogDebug(log, "Parsed value: '%s'", value->c_str()); + } + + if (!value.HasValue()) + { + return indicators.NonCompliant("Systemd option " + params.section + "/" + params.option + " is not configured"); + } + + // Some value has been found + OsConfigLogDebug(log, "Systemd option '%s/%s' value: '%s'", params.section.c_str(), params.option.c_str(), value->c_str()); + const auto comparisonResult = Compare(value.Value(), params.value, params.expression, context); + if (!comparisonResult.HasValue()) + { + OsConfigLogError(log, "Failed to compare systemd option '%s/%s' value '%s' with '%s'", params.section.c_str(), params.option.c_str(), + params.value.c_str(), value->c_str()); + return comparisonResult.Error(); + } + + if (comparisonResult.Value()) + { + return indicators.Compliant("Systemd option '" + params.section + "/" + params.option + "' matches expected value '" + params.value + "'"); + } + return indicators.NonCompliant("Systemd option '" + params.section + "/" + params.option + "' does not match expected value '" + params.value + "'"); +} + +} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.h b/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.h new file mode 100644 index 0000000000..97dc4e5ef0 --- /dev/null +++ b/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef COMPLIANCEENGINE_PROCEDURES_ENSURE_SYSTEMD_PARAMETER_H +#define COMPLIANCEENGINE_PROCEDURES_ENSURE_SYSTEMD_PARAMETER_H + +#include +#include + +namespace ComplianceEngine +{ +struct SystemdParameterParams +{ + /// Parameter name + std::string parameter; + + /// Regex for the value + regex valueRegex; + + /// Config filename + Optional file; + + /// Directory to search for config files + Optional dir; +}; + +Result AuditSystemdParameter(const SystemdParameterParams& params, IndicatorsTree& indicators, ContextInterface& context); + +enum class SystemdParameterExpression +{ + /// label: lt + LessThan, + + /// label: le + LessOrEqual, + + /// label: gt + GreaterThan, + + /// label: ge + GreaterOrEqual, + + /// label: eq + Equal, +}; + +// The V4 version reflects the SCE v4 version of the systemd parameter checks. +// Keeping the old version as the semantics changed significantly between this +// and older versions of the scripts. +struct EnsureSystemdParameterV4Params +{ + /// Main configuration filename + std::string file; + + /// Systemd section name + std::string section; + + /// Systemd option name + std::string option; + + /// Expression + SystemdParameterExpression expression; + + /// Value to compare using provided expression + std::string value; +}; + +Result AuditEnsureSystemdParameterV4(const EnsureSystemdParameterV4Params& params, IndicatorsTree& indicators, ContextInterface& context); +} // namespace ComplianceEngine +#endif // COMPLIANCEENGINE_PROCEDURES_ENSURE_SYSTEMD_PARAMETER_H diff --git a/src/modules/complianceengine/src/lib/procedures/SystemdConfig.cpp b/src/modules/complianceengine/src/lib/procedures/SystemdConfig.cpp deleted file mode 100644 index 22aecf1ebb..0000000000 --- a/src/modules/complianceengine/src/lib/procedures/SystemdConfig.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#include -#include -#include -#include -#include -#include -#include -#include - -using ComplianceEngine::Optional; - -namespace ComplianceEngine -{ - -namespace -{ -typedef std::map> SystemdConfigMap_t; - -Result GetSystemdConfig(SystemdConfigMap_t& config, const std::string& filename, ContextInterface& context) -{ - auto result = context.ExecuteCommand("/usr/bin/systemd-analyze cat-config " + filename); - if (!result.HasValue()) - { - OsConfigLogError(context.GetLogHandle(), "Failed to execute systemd-analyze command: %s", result.Error().message.c_str()); - OSConfigTelemetryStatusTrace("ExecuteCommand", result.Error().code); - return result.Error(); - } - std::istringstream stream(result.Value()); - std::string line; - std::string currentConfig = ""; - while (std::getline(stream, line)) - { - if (line.empty()) - { - continue; - } - if ('#' == line[0]) - { - if ((line.size() > strlen("# .conf")) && ('#' == line[0]) && (".conf" == line.substr(line.size() - strlen(".conf")))) - { - currentConfig = line.substr(2); - } - continue; - } - size_t eqSign = line.find('='); - if (eqSign == std::string::npos) - { - OsConfigLogError(context.GetLogHandle(), "Invalid line in systemd config: %s", line.c_str()); - OSConfigTelemetryStatusTrace("getline", EINVAL); - continue; - } - std::string key = line.substr(0, eqSign); - std::string value = line.substr(eqSign + 1); - config[key] = std::make_pair(value, currentConfig); - } - return true; -} -} // namespace - -Result AuditSystemdParameter(const SystemdParameterParams& params, IndicatorsTree& indicators, ContextInterface& context) -{ - auto log = context.GetLogHandle(); - - if (!params.dir.HasValue() && !params.file.HasValue()) - { - OsConfigLogError(log, "Error: SystemdParameter: neither 'file' nor 'dir' argument is provided"); - OSConfigTelemetryStatusTrace("dir.empty && filename.empty", EINVAL); - return Error("Neither 'file' nor 'dir' argument is provided"); - } - if (params.dir.HasValue() && params.file.HasValue()) - { - OsConfigLogError(log, "Error: SystemdParameter: both 'file' and 'dir' arguments are provided, only one is allowed"); - OSConfigTelemetryStatusTrace("one dir or file only", EINVAL); - return Error("Both 'file' and 'dir' arguments are provided, only one is allowed"); - } - - SystemdConfigMap_t config; - if (params.file.HasValue()) - { - OsConfigLogDebug(log, "Getting systemd config for file '%s'", params.file->c_str()); - auto result = GetSystemdConfig(config, params.file.Value(), context); - if (!result.HasValue()) - { - OsConfigLogError(log, "Failed to get systemd config for file '%s' - %s", params.file->c_str(), result.Error().message.c_str()); - OSConfigTelemetryStatusTrace("GetSystemdConfig", result.Error().code); - return result.Error(); - } - } - else - { - OsConfigLogDebug(log, "Getting systemd config for directory '%s'", params.dir->c_str()); - bool anySuccess = false; - char* paths[] = {const_cast(params.dir->c_str()), nullptr}; - FTS* file_system = fts_open(paths, FTS_NOCHDIR | FTS_PHYSICAL, nullptr); - if (!file_system) - { - OsConfigLogError(log, "Failed to open directory '%s' with fts", params.dir->c_str()); - OSConfigTelemetryStatusTrace("fts_open", EINVAL); - return Error("Failed to open directory '" + params.dir.Value() + "'"); - } - - FTSENT* node = nullptr; - while ((node = fts_read(file_system)) != nullptr) - { - if (node->fts_info == FTS_F) - { - std::string filePath = node->fts_path; - if (filePath.size() >= strlen(".conf") && filePath.substr(filePath.size() - 5) == ".conf") - { - OsConfigLogDebug(log, "Getting systemd config for file '%s' in directory '%s'", filePath.c_str(), params.dir->c_str()); - auto result = GetSystemdConfig(config, filePath, context); - if (!result.HasValue()) - { - OsConfigLogError(log, "Failed to get systemd config for file '%s' - %s", filePath.c_str(), result.Error().message.c_str()); - OSConfigTelemetryStatusTrace("GetSystemdConfig", result.Error().code); - } - else - { - anySuccess = true; - OsConfigLogDebug(log, "Successfully got systemd config for file '%s'", filePath.c_str()); - } - } - } - } - fts_close(file_system); - if (!anySuccess) - { - OsConfigLogError(log, "No valid systemd config files found in directory '%s'", params.dir->c_str()); - OSConfigTelemetryStatusTrace("fts_close", EINVAL); - return Error("No valid systemd config files found in directory '" + params.dir.Value() + "'"); - } - } - - auto paramIt = config.find(params.parameter); - if (paramIt == config.end()) - { - OsConfigLogInfo(log, "Parameter '%s' not found", params.parameter.c_str()); - return indicators.NonCompliant("Parameter '" + params.parameter + "' not found"); - } - - OsConfigLogDebug(log, "Parameter '%s' found in file '%s' with value '%s'", params.parameter.c_str(), paramIt->second.second.c_str(), - paramIt->second.first.c_str()); - if (!regex_match(paramIt->second.first, params.valueRegex)) - { - OsConfigLogInfo(log, "Parameter '%s' in file '%s' does not match regex", params.parameter.c_str(), paramIt->second.second.c_str()); - return indicators.NonCompliant("Parameter '" + params.parameter + "' value '" + paramIt->second.first + "' in file '" + paramIt->second.second + - "' does not match regex"); - } - else - { - return indicators.Compliant("Parameter '" + params.parameter + "' found in file '" + paramIt->second.second + "' with value '" + - paramIt->second.first + "'"); - } -} - -} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/lib/procedures/SystemdConfig.h b/src/modules/complianceengine/src/lib/procedures/SystemdConfig.h deleted file mode 100644 index e48c9746f8..0000000000 --- a/src/modules/complianceengine/src/lib/procedures/SystemdConfig.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#ifndef COMPLIANCEENGINE_PROCEDURES_ENSURE_SYSTEMD_CONFIG_H -#define COMPLIANCEENGINE_PROCEDURES_ENSURE_SYSTEMD_CONFIG_H - -#include -#include - -namespace ComplianceEngine -{ -struct SystemdParameterParams -{ - /// Parameter name - std::string parameter; - - /// Regex for the value - regex valueRegex; - - /// Config filename - Optional file; - - /// Directory to search for config files - Optional dir; -}; - -Result AuditSystemdParameter(const SystemdParameterParams& params, IndicatorsTree& indicators, ContextInterface& context); -} // namespace ComplianceEngine -#endif // COMPLIANCEENGINE_PROCEDURES_ENSURE_SYSTEMD_CONFIG_H diff --git a/src/modules/complianceengine/tests/procedures/SystemdConfigTest.cpp b/src/modules/complianceengine/tests/procedures/SystemdConfigTest.cpp index 3438f532bd..8a1b60a903 100644 --- a/src/modules/complianceengine/tests/procedures/SystemdConfigTest.cpp +++ b/src/modules/complianceengine/tests/procedures/SystemdConfigTest.cpp @@ -3,16 +3,20 @@ #include "MockContext.h" -#include +#include #include #include #include +using ComplianceEngine::AuditEnsureSystemdParameterV4; using ComplianceEngine::AuditSystemdParameter; +using ComplianceEngine::CompactListFormatter; +using ComplianceEngine::EnsureSystemdParameterV4Params; using ComplianceEngine::Error; using ComplianceEngine::IndicatorsTree; using ComplianceEngine::Result; using ComplianceEngine::Status; +using ComplianceEngine::SystemdParameterExpression; using ComplianceEngine::SystemdParameterParams; using ::testing::Return; @@ -25,6 +29,10 @@ class SystemdConfigTest : public ::testing::Test void SetUp() override { mIndicators.Push("SystemdParameter"); + EXPECT_CALL(mContext, ExecuteCommand("readlink -e /bin/systemd-analyze")) + .WillRepeatedly(Return(Result("/usr/bin/systemd-analyze"))); + EXPECT_CALL(mContext, ExecuteCommand("readlink -e /usr/bin/systemd-analyze")) + .WillRepeatedly(Return(Result("/usr/bin/systemd-analyze"))); } void TearDown() override @@ -265,3 +273,370 @@ TEST_F(SystemdConfigTest, FileParameterWithSpecialCharacters) ASSERT_TRUE(result.HasValue()); ASSERT_EQ(result.Value(), Status::Compliant); } + +TEST_F(SystemdConfigTest, V4_NonExistentFile) +{ + EnsureSystemdParameterV4Params params; + params.file = "nonexistentfile"; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "1"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + EXPECT_EQ(result.Value(), Status::NonCompliant); +} + +TEST_F(SystemdConfigTest, V4_FileCommandExecutionFails) +{ + const auto filename = mContext.MakeTempfile(""); + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))) + .WillOnce(Return(Result(Error("Command execution failed", -1)))); + // EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))) + // .WillOnce(Return(Result("[test]\nfoo=1"))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "bar"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_FALSE(result.HasValue()); + ASSERT_EQ(result.Error().message, "Command execution failed"); +} + +TEST_F(SystemdConfigTest, V4_SectionNotFound) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[foo]\n" + "bar=baz\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "bar"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::NonCompliant); +} + +TEST_F(SystemdConfigTest, V4_OptionNotFound) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "bar=baz\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "bar"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::NonCompliant); +} + +TEST_F(SystemdConfigTest, V4_Match_1) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=bar\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "bar"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::Compliant); +} + +TEST_F(SystemdConfigTest, V4_Mismatch_1) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=baz\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "bar"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::NonCompliant); +} + +TEST_F(SystemdConfigTest, V4_Mismatch_2) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=bar\n" + "foo=baz\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "bar"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::NonCompliant); +} + +TEST_F(SystemdConfigTest, V4_Match_2) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=baz\n" + "foo=bar\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "bar"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::Compliant); +} + +TEST_F(SystemdConfigTest, V4_Match_4) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=\"baz\"\n" + " foo = \"bar\"\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::Equal; + params.value = "bar"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::Compliant); +} + +TEST_F(SystemdConfigTest, V4_Match_5) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=3\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::LessThan; + params.value = "4"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::Compliant); +} + +TEST_F(SystemdConfigTest, V4_Match_6) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=5\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::GreaterThan; + params.value = "4"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::Compliant); +} + +TEST_F(SystemdConfigTest, V4_Match_7) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=4\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::LessOrEqual; + params.value = "4"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::Compliant); +} + +TEST_F(SystemdConfigTest, V4_Match_8) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=4\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::GreaterOrEqual; + params.value = "4"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::Compliant); +} + +TEST_F(SystemdConfigTest, V4_Misatch_3) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=4\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::LessThan; + params.value = "4"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::NonCompliant); +} + +TEST_F(SystemdConfigTest, V4_Misatch_4) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=4\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::GreaterThan; + params.value = "4"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::NonCompliant); +} + +TEST_F(SystemdConfigTest, V4_Misatch_5) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=5\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::LessOrEqual; + params.value = "4"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::NonCompliant); +} + +TEST_F(SystemdConfigTest, V4_Misatch_6) +{ + const auto filename = mContext.MakeTempfile(""); + const std::string systemdOutput = + "# /etc/systemd/test.conf\n" + "[test]\n" + "foo=3\n"; + + EXPECT_CALL(mContext, ExecuteCommand(::testing::HasSubstr("/usr/bin/systemd-analyze cat-config " + filename))).WillOnce(Return(Result(systemdOutput))); + + EnsureSystemdParameterV4Params params; + params.file = filename; + params.section = "test"; + params.option = "foo"; + params.expression = SystemdParameterExpression::GreaterOrEqual; + params.value = "4"; + + const auto result = AuditEnsureSystemdParameterV4(params, mIndicators, mContext); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value(), Status::NonCompliant); +} From 2989a899ad6634e004da1144cb2a40ca420f0f4a Mon Sep 17 00:00:00 2001 From: Robert Wojciechowski Date: Tue, 25 Nov 2025 14:47:38 +0100 Subject: [PATCH 2/2] Add missing file --- .../EnsureSystemdParameter.schema.json | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.schema.json diff --git a/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.schema.json b/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.schema.json new file mode 100644 index 0000000000..3581a199f5 --- /dev/null +++ b/src/modules/complianceengine/src/lib/procedures/EnsureSystemdParameter.schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "audit": { + "anyOf": [ + { + "type": "object", + "required": [ + "EnsureSystemdParameterV4" + ], + "additionalProperties": false, + "properties": { + "EnsureSystemdParameterV4": { + "type": "object", + "required": [ + "file", + "section", + "option", + "expression", + "value" + ], + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "Main configuration filename" + }, + "section": { + "type": "string", + "description": "Systemd section name" + }, + "option": { + "type": "string", + "description": "Systemd option name" + }, + "expression": { + "type": "string", + "description": "Expression" + }, + "value": { + "type": "string", + "description": "Value to compare using provided expression" + } + } + } + } + }, + { + "type": "object", + "required": [ + "SystemdParameter" + ], + "additionalProperties": false, + "properties": { + "SystemdParameter": { + "type": "object", + "required": [ + "parameter", + "valueRegex" + ], + "additionalProperties": false, + "properties": { + "parameter": { + "type": "string", + "description": "Parameter name" + }, + "valueRegex": { + "type": "string", + "description": "Regex for the value" + }, + "file": { + "type": "string", + "description": "Config filename" + }, + "dir": { + "type": "string", + "description": "Directory to search for config files" + } + } + } + } + } + ] + }, + "remediation": { + "anyOf": [] + } + } +}