diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3ce3aa4afd..6b40d7982d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,8 +26,5 @@ } }, "workspaceFolder": "/workspaces/azure-osconfig", - "mounts": [ - "source=${localWorkspaceFolder}/../azcorelinux-Compliance-AugmentationEngine,target=/workspaces/azcorelinux-Compliance-AugmentationEngine,type=bind,consistency=cached" - ], "remoteUser": "root" } diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 155900440a..fef07b6c1e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,12 +13,12 @@ The architecture has three layers: **Adapters** (IoT Hub PnP agent, RC/DC watche ``` src/ # All source code CMakeLists.txt # Root CMake file (project config, vcpkg integration, build options) - vcpkg.json # Dependencies: openssl, curl, lua, sqlite3, nlohmann-json, gtest + vcpkg.json # Dependencies: openssl, curl, sqlite3, nlohmann-json, gtest vcpkg-configuration.json # vcpkg registry baseline adapters/ # Agent adapters (PnP IoT Hub client, MC machine config) pnp/ # PnP agent (main binary: /usr/bin/osconfig) azure-iot-sdk-c/ # Git submodule - Azure IoT C SDK - mc/ # Machine configuration adapters (ASB, SSH, compliance engine) + mc/ # Machine configuration adapters (ASB, SSH) platform/ # Management Platform daemon (/usr/bin/osconfig-platform) inc/Mpi.h # MPI interface header Main.c, MpiServer.c, ModulesManager.c, MmiClient.c @@ -39,7 +39,6 @@ src/ # All source code securitybaseline/ # SecurityBaseline module configuration/ # Configuration module deviceinfo/ # DeviceInfo module - complianceengine/ # ComplianceEngine module (Lua-based evaluator) test/ # Module test harness (moduletest tool) recipes/ # Test recipe JSON files samples/ # Sample module (C++) @@ -88,7 +87,7 @@ ctest --test-dir . --output-on-failure -j$(nproc) - ~960 unit tests using Google Test. Some tests requiring root or special filesystem permissions will be **skipped** (not failed) when run as non-root. ~10 tests involving file access may fail without root. - Tests are registered per-module and per-library in their respective `tests/` subdirectories. -- Test binaries are in: `build/common/commonutils/`, `build/platform/tests/`, `build/modules/commandrunner/tests/`, `build/modules/complianceengine/tests/`, `build/modules/configuration/tests/`. +- Test binaries are in: `build/common/commonutils/`, `build/platform/tests/`, `build/modules/commandrunner/tests/`, `build/modules/configuration/tests/`. - Total test time: ~10 seconds. ### Formatting and Linting (required before PR) @@ -98,9 +97,9 @@ Always run pre-commit before submitting changes: python3 -m pre_commit run --all-files ``` -This runs: trailing whitespace fix, end-of-file fix, LF line endings, clang-format (v14, on complianceengine + telemetry files only), clang-tidy (on complianceengine + telemetry C++ files only), and compliance engine interface generation. +This runs: trailing whitespace fix, end-of-file fix, LF line endings, clang-format (v14, on telemetry files only), and clang-tidy (on telemetry C++ files only). -**Important**: clang-format and clang-tidy in pre-commit only apply to files in `src/modules/complianceengine/`, `src/compliance-engine-assessor/`, and `src/common/telemetry/`. Other C/C++ files are not auto-formatted but must follow the style in `docs/style.md`. +**Important**: clang-format and clang-tidy in pre-commit only apply to files in `src/common/telemetry/`. Other C/C++ files are not auto-formatted but must follow the style in `docs/style.md`. ## CI Checks on Pull Requests @@ -127,177 +126,4 @@ Each module is a shared library (`.so`) implementing the MMI API (`MmiOpen`, `Mm 3. Register in `src/modules/CMakeLists.txt` using the `add_module()` function 4. Add test recipes in `src/modules/test/recipes/` -**Only 5 modules are actively built:** commandrunner, securitybaseline, configuration, deviceinfo, complianceengine. Other module directories (adhs, firewall, hostname, networking, pmc, tpm, ztsi) exist but are not included in the default build. - -## ComplianceEngine Module (Important Module) - -The ComplianceEngine is the most complex and important module. It evaluates security compliance rules defined as **rule payloads**, using a combination of **logical combinators** (`allOf`, `anyOf`, `not`), **built-in C++ procedures**, and **Lua scripts**. It supports both **audit** (check compliance) and **remediation** (fix non-compliance) actions. - -**Upstream producer**: Rule payloads are generated by the **Compliance Augmentation Engine** (`azcorelinux-Compliance-AugmentationEngine` repo), which transforms CIS XCCDF benchmarks into these JSON structures and base64-encodes them into MOF files. The same concept is referred to as "JSON conditionals", "calling convention", or "mofJson" in that repo. The payload structure (`{audit, remediate, parameters}`) is identical—produced upstream, consumed here. - -### Architecture - -``` -src/modules/complianceengine/ - src/ - lib/ # Core library (complianceenginelib) - Engine.h/.cpp # Top-level MMI handler, manages rule database - Evaluator.h/.cpp # Recursive rule evaluator (allOf/anyOf/not/Lua/builtin dispatch) - Procedure.h/.cpp # Stores a single rule's audit/remediate JSON + parameters - ProcedureMap.h/.cpp # AUTO-GENERATED - maps procedure names → function pointers - Bindings.h # Template framework connecting params structs → string args - BindingParsers.h/.cpp # Type parsers (string, int, bool, mode_t, regex, Pattern) - GenInterface.py # Code generator: parses .h files → generates ProcedureMap.h/.cpp - Indicators.h/.cpp # Tree of compliance/non-compliance status messages - ContextInterface.h/.cpp # Abstract interface for system access (commands, files, logging) - LuaEvaluator.h/.cpp # Lua script execution engine - payload.schema.json # JSON Schema for rule payloads (allOf/anyOf/not/Lua/procedures) - procedures/ # Built-in procedure implementations (one triad per procedure) - so/ # Module .so entry point (ComplianceEngineModule.c) - assessor/ # CLI assessor tool - lua-evaluator/ # Lua evaluator subdirectory - tests/ # Unit tests - procedures/ # Per-procedure test files -``` - -### How Rule Evaluation Works - -A **rule payload** is a JSON object with `audit`, optional `remediate`, and optional `parameters` fields. This is the exact structure produced by the Compliance Augmentation Engine (where it is called "JSON conditionals" or "calling convention") and base64-encoded into the MOF `ProcedureObjectValue` field. The `audit` and `remediate` fields contain a recursive expression tree: - -```json -{ - "audit": { - "allOf": [ - { "EnsureFilePermissions": { "filename": "/etc/passwd", "permissions": "0644" } }, - { "anyOf": [ - { "PackageInstalled": { "packageName": "openssh-server" } }, - { "not": { "EnsureFileExists": { "filename": "/etc/ssh/sshd_config" } } } - ]}, - { "Lua": { "script": "return Compliant('ok')" } } - ] - }, - "parameters": { "myParam": "defaultValue" } -} -``` - -The `Evaluator` recursively processes each node in the expression tree (`Evaluator.cpp`): -- **`allOf`**: Array of sub-expressions. Returns `NonCompliant` on first failure (short-circuit AND). -- **`anyOf`**: Array of sub-expressions. Returns `Compliant` on first success (short-circuit OR). -- **`not`**: Inverts the result. Audit-only (no remediation through `not`). -- **`Lua`**: Runs an inline Lua script via `LuaEvaluator`. -- **Any other key**: Looked up as a built-in procedure name in `Evaluator::mProcedureMap`. - -Procedure arguments support **parameter substitution**: a value starting with `$` (e.g., `"$myParam"`) is replaced with the value from the rule's `parameters` map. The Compliance Augmentation Engine produces these `$paramName` placeholders with default values in the `parameters` dict; user overrides arrive via `DesiredObjectValue` in the MOF (`key=value` pairs) and are applied by `Procedure::UpdateUserParameters()`. - -### Built-in Procedures - -Built-in procedure source files live in `src/modules/complianceengine/src/lib/procedures/`. A single file can contain multiple related procedures — for example, `EnsureFilePermissions.h/.cpp` defines both `EnsureFilePermissions` and `EnsureFilePermissionsCollection`. The file naming reflects the logical grouping, not a 1:1 mapping to procedure names. - -Each source file consists of up to three parts: - -| File | Purpose | -|------|---------| -| `.h` | Params struct(s) + audit/remediate function declarations for one or more procedures | -| `.cpp` | Implementation(s) | -| `.schema.json` | JSON Schema fragment(s) with `definitions.audit` and `definitions.remediation` sections for each procedure in the file | - -Each procedure must implement an Audit function and may optionally implement a Remediate function: -- `Result Audit(const Params& params, IndicatorsTree& indicators, ContextInterface& context)` — **required** -- `Result Remediate(const Params& params, IndicatorsTree& indicators, ContextInterface& context)` — **optional** - -Functions return `Status::Compliant` or `Status::NonCompliant` via `indicators.Compliant("msg")` / `indicators.NonCompliant("msg")`, or `Error(...)` on failure. - -**Important:** A single file can contain multiple related procedures (e.g., `EnsureFilePermissions.h` defines both `EnsureFilePermissions` and `EnsureFilePermissionsCollection`). The filename does not need to match any individual procedure name — group related procedures together when it makes sense. When adding a new procedure, consider whether it logically belongs in an existing file before creating a new one. - -### How to Add a New Procedure - -1. **Add the procedure to a header** in `procedures/` (new or existing `.h` file): Define a params struct and declare audit/remediate functions. - - Struct fields use types: `std::string`, `int`, `bool`, `mode_t`, `regex`, `Pattern`, `Optional`, `Separated`, or enum types. - - Document each field with `///` comments (used by GenInterface.py). Add `/// pattern: ` for validation. - - ```cpp - struct AuditParams - { - /// Description of the parameter - std::string requiredParam; - - /// Optional parameter description - Optional optionalParam; - }; - - Result Audit(const AuditParams& params, IndicatorsTree& indicators, ContextInterface& context); - ``` - -2. **Add the implementation** to a `.cpp` file in `procedures/` (matching the header file, not necessarily the procedure name). - -3. **Add or update the schema** in a `.schema.json` file in `procedures/` with `definitions.audit` and `definitions.remediation` sections for the new procedure. - -4. **Run `GenInterface.py`** (or `pre-commit`): The script parses all procedure headers and **auto-generates** `ProcedureMap.h` and `ProcedureMap.cpp`. These files: - - Include all procedure headers. - - Define `Bindings` specializations (field name arrays + member pointer tuples). - - Define `MapEnum()` specializations for any custom enums. - - Populate `Evaluator::mProcedureMap` with `{name, {MakeHandler(Audit...), MakeHandler(Remediate...)}}`. - - **NEVER edit `ProcedureMap.h` or `ProcedureMap.cpp` manually** — they are regenerated by `GenInterface.py`. - -5. **Register in CMakeLists.txt**: Add the `.cpp` to the `PROCEDURES` list and the `.schema.json` to the `SCHEMAS` list in `src/modules/complianceengine/src/lib/CMakeLists.txt`. The build enforces that every `.cpp` in `procedures/` is listed and every procedure has a matching schema. - -6. **Add unit tests** in `tests/procedures/Test.cpp`. Use `MockContext` from `tests/MockContext.h` to mock file/command access. - -7. **Run pre-commit** to regenerate the ProcedureMap files and validate formatting: - ```bash - python3 -m pre_commit run --all-files - ``` - -### Key Types for Procedure Development - -- `IndicatorsTree`: Call `indicators.Compliant("message")` or `indicators.NonCompliant("message")` to record results. -- `ContextInterface`: Use `context.ExecuteCommand(cmd)` and `context.GetFileContents(path)` for system access. Never call system functions directly — this enables unit testing with `MockContext`. -- `Result`: Either holds a value (`HasValue()`) or an `Error`. Return `Error("msg", errno_code)` on failure. -- `Optional`: For optional procedure parameters. `HasValue()` checks if set. -- `Separated`: For pipe/comma-separated list parameters (e.g., `Separated`). - -### Enum Parameters - -Enum types allow procedure parameters to accept a fixed set of string labels from JSON. To add an enum parameter: - -1. **Define the enum** in the procedure header file, placing it **before** the params struct that uses it. Each enum value must have a `/// label: ` comment specifying the string used in JSON rule payloads: - - ```cpp - enum class PackageManagerType - { - /// label: autodetect - Autodetect, - - /// label: rpm - RPM, - - /// label: dpkg - DPKG, - }; - ``` - -2. **Use the enum type** in the params struct: - - ```cpp - struct PackageInstalledParams - { - /// Package name - std::string packageName; - - /// Package manager, autodetected by default - Optional packageManager; - }; - ``` - -3. **Run `GenInterface.py`** (or `pre-commit`): It parses the `/// label:` comments and auto-generates a `MapEnum()` specialization in `ProcedureMap.h` that maps JSON string labels to C++ enum values. **Do not write this mapping manually.** - -In rule payloads, the enum value is specified by its label string: `{ "PackageInstalled": { "packageName": "nftables", "packageManager": "rpm" } }`. - -### Testing ComplianceEngine - -Unit tests are in `src/modules/complianceengine/tests/` and `tests/procedures/`. Integration tests use recipe JSON files in `src/modules/test/recipes/complianceengine/`. Run: - -```bash -cd build && ctest -R complianceengine --output-on-failure -j$(nproc) -``` +**Only 4 modules are actively built:** commandrunner, securitybaseline, configuration, deviceinfo. Other module directories (adhs, firewall, hostname, networking, pmc, tpm, ztsi) exist but are not included in the default build. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 30f6d026fe..a381a5a3f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,44 +47,17 @@ repos: additional_dependencies: [clang-tidy] files: | (?x)( - ^src/modules/complianceengine/src/.*\.h$| - ^src/modules/complianceengine/src/.*\.cpp$| - ^src/compliance-engine-assessor/.*\.hpp$| - ^src/compliance-engine-assessor/.*\.cpp$| ^src/common/telemetry/.*\.h$| ^src/common/telemetry/.*\.hpp$| ^src/common/telemetry/.*\.c$ ) - - repo: local - hooks: - - id: compliance-engine-interface - name: Generate symbolic compliance engine interface - types_or: [c++, json] - entry: src/modules/complianceengine/src/lib/GenInterface.py - language: python - require_serial: true - files: | - (?x)( - ^src/modules/complianceengine/src/lib/payload.schema.json$| - ^src/modules/complianceengine/src/lib/procedures/.*\.h$| - ^src/modules/complianceengine/src/lib/procedures/.*\.schema.json$ - ) - repo: https://github.com/pre-commit/mirrors-clang-format rev: v14.0.6 hooks: - id: clang-format files: | (?x)( - ^src/modules/complianceengine/.*\.h$| - ^src/modules/complianceengine/.*\.cpp$| - ^src/compliance-engine-assessor/.*\.hpp$| - ^src/compliance-engine-assessor/.*\.cpp$| ^src/common/telemetry/.*\.h$| ^src/common/telemetry/.*\.hpp$| ^src/common/telemetry/.*\.c$ ) - exclude: | - (?x)( - ^src/modules/complianceengine/src/lib/ProcedureMap.h$| - ^src/modules/complianceengine/src/lib/ProcedureMap.cpp$ - ) diff --git a/README.md b/README.md index 4ee00808d3..78ae9743dc 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ Source | Destination | Description [src/modules/commandrunner/](src/modules/commandrunner/) | /usr/lib/osconfig/commandrunner.so | The CommandRunner module binary [src/modules/configuration/](src/modules/configuration/) | /usr/lib/osconfig/configuration.so | The Configuration module binary [src/modules/securitybaseline/](src/modules/securitybaseline/) | /usr/lib/osconfig/securitybaseline.so | The SecurityBaseline module binary -[src/modules/complianceengine/](src/modules/complianceengine/) | /usr/lib/osconfig/complianceengine.so | The ComplianceEngine module binary [src/common/telemetry/](src/common/telemetry/) | /var/lib/osconfig/telemetry | The OSConfig telemetry directory ### Enable and start OSConfig for the first time diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 18fb741a19..90e43c1cf4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -36,7 +36,6 @@ option(BUILD_TELEMETRY "Build telemetry" 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/adapters/mc/CMakeLists.txt b/src/adapters/mc/CMakeLists.txt index b4d56e917c..5513ecb086 100644 --- a/src/adapters/mc/CMakeLists.txt +++ b/src/adapters/mc/CMakeLists.txt @@ -84,4 +84,3 @@ add_compile_options("-Wall;-Wextra;-Wunused;-Werror;-Wformat;-Wformat-security;- add_subdirectory(ssh) add_subdirectory(asb) -add_subdirectory(complianceengine) diff --git a/src/adapters/mc/complianceengine/Baseline.c b/src/adapters/mc/complianceengine/Baseline.c deleted file mode 100644 index 14a1e19224..0000000000 --- a/src/adapters/mc/complianceengine/Baseline.c +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#include "../Common.h" -#include "ComplianceEngineInterface.h" - -static MMI_HANDLE gComplianceEngine = NULL; -static const char gComponentName[] = "ComplianceEngine"; - -int BaselineIsValidResourceIdRuleId(const char* resourceId, const char* ruleId, const char* payloadKey, OsConfigLogHandle log) -{ - UNUSED(resourceId); - UNUSED(ruleId); - UNUSED(payloadKey); - UNUSED(log); - return 0; -} - -int BaselineIsCorrectDistribution(const char* payloadKey, OsConfigLogHandle log) -{ - return ComplianceEngineCheckApplicability(gComplianceEngine, payloadKey, log); -} - -// This function is called in library constructor in OsConfigResource.c -void BaselineInitialize(OsConfigLogHandle log) -{ - ComplianceEngineInitialize(log); - gComplianceEngine = ComplianceEngineMmiOpen(gComponentName, -1); -} - -// This function is called in library destructor in OsConfigResource.c -void BaselineShutdown(OsConfigLogHandle log) -{ - UNUSED(log); - if (NULL == gComplianceEngine) - { - return; - } - - ComplianceEngineMmiClose(gComplianceEngine); - ComplianceEngineShutdown(); - gComplianceEngine = NULL; -} - -int BaselineMmiGet(const char* componentName, const char* objectName, char** payload, int* payloadSizeBytes, unsigned int maxPayloadSizeBytes, OsConfigLogHandle log) -{ - if ((NULL == componentName) || (NULL == objectName)) - { - OsConfigLogError(log, "BaselineMmiGet called with invalid arguments"); - return EINVAL; - } - - int result = ComplianceEngineMmiGet(gComplianceEngine, componentName, objectName, payload, payloadSizeBytes); - if (MMI_OK != result) - { - OsConfigLogError(log, "BaselineMmiGet(%s, %s) failed: %d", componentName, objectName, result); - return result; - } - - if ((NULL != *payload) && (*payloadSizeBytes > 0) & (maxPayloadSizeBytes > 0) && ((unsigned)*payloadSizeBytes > maxPayloadSizeBytes)) - { - OsConfigLogInfo(log, "BaselineMmiGet(%s, %s) payload truncated from %d to %u bytes", componentName, objectName, *payloadSizeBytes, maxPayloadSizeBytes); - *payloadSizeBytes = (int)maxPayloadSizeBytes; - *payload[*payloadSizeBytes] = '\0'; - } - - return MMI_OK; -} - -int BaselineMmiSet(const char* componentName, const char* objectName, const char* payload, const int payloadSizeBytes, OsConfigLogHandle log) -{ - UNUSED(log); - return ComplianceEngineMmiSet(gComplianceEngine, componentName, objectName, payload, payloadSizeBytes); -} diff --git a/src/adapters/mc/complianceengine/CMakeLists.txt b/src/adapters/mc/complianceengine/CMakeLists.txt deleted file mode 100644 index a3461ee371..0000000000 --- a/src/adapters/mc/complianceengine/CMakeLists.txt +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Compliance module for Linux -project(OsConfigResourceComplianceEngine) -if (BUILD_FUZZER) - add_link_options("LINKER:-z,defs -shared-libasan") -else (BUILD_FUZZER) - add_link_options("LINKER:-z,defs") -endif (BUILD_FUZZER) -add_library(OsConfigResourceComplianceEngine - SHARED - ../module.c - ../schema.c - ../OsConfigResource.c - Baseline.c) - -target_link_libraries(OsConfigResourceComplianceEngine - PRIVATE - commonutils - logging - mpiclient - parsonlib - complianceenginelib) - -if(BUILD_TELEMETRY) - list(APPEND STAGING_COMMANDS - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${BINARY_DIR}/bin/telemetrybin" ${PROJECT_BINARY_DIR}/StagingComplianceEngineShell/Modules/DscNativeResources/OsConfigResource/OSConfigTelemetry) -endif() - -add_custom_target(stage_create_compliance_engine_shell_zip - COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/StagingComplianceEngineShell - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/ComplianceEngineShell.metaconfig.json" ${PROJECT_BINARY_DIR}/StagingComplianceEngineShell/ - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/ComplianceEngineShell.mof" ${PROJECT_BINARY_DIR}/StagingComplianceEngineShell - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ ${PROJECT_BINARY_DIR}/StagingComplianceEngineShell/Modules/DscNativeResources/OsConfigResource/libOsConfigResource.so - COMMAND ${CMAKE_COMMAND} -E copy_if_different $/$ ${PROJECT_BINARY_DIR}/StagingComplianceEngineShell/ - ${STAGING_COMMANDS} - DEPENDS OsConfigResourceComplianceEngine ComplianceEngineShell.mof complianceengineschema $<$:telemetry-external>) - -add_custom_target(create_compliance_engine_shell_zip ALL - BYPRODUCTS ${OsConfigRootBinaryDir}/ComplianceEngineShell.zip - COMMAND ${CMAKE_COMMAND} -E tar "cfv" "${OsConfigRootBinaryDir}/ComplianceEngineShell.zip" --format=zip . - DEPENDS stage_create_compliance_engine_shell_zip - WORKING_DIRECTORY ${PROJECT_BINARY_DIR}/StagingComplianceEngineShell/) - -add_subdirectory(example) diff --git a/src/adapters/mc/complianceengine/ComplianceEngineShell.metaconfig.json b/src/adapters/mc/complianceengine/ComplianceEngineShell.metaconfig.json deleted file mode 100644 index 397169cfdc..0000000000 --- a/src/adapters/mc/complianceengine/ComplianceEngineShell.metaconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Type": "AuditAndSet", - "Version": "1.0.0" -} diff --git a/src/adapters/mc/complianceengine/ComplianceEngineShell.mof b/src/adapters/mc/complianceengine/ComplianceEngineShell.mof deleted file mode 100644 index 8238178435..0000000000 --- a/src/adapters/mc/complianceengine/ComplianceEngineShell.mof +++ /dev/null @@ -1,26 +0,0 @@ -instance of OsConfigResource as $OsConfigResource0ref -{ - ResourceID = ""; - RuleId = "00000000-0000-0000-0000-000000000000"; - PayloadKey = ""; - ComponentName = "Compliance"; - ProcedureObjectName = "procedureRule"; - ProcedureObjectValue = "eyJhdWRpdCI6eyJhbGxPZiI6W119fQ=="; - ReportedObjectName = "auditRule"; - ExpectedObjectValue = "PASS"; - InitObjectName = "initRule"; - DesiredObjectValue = ""; - ModuleName = "GuestConfiguration"; - ModuleVersion = "1.0.0"; - ConfigurationName = "CIS"; - SourceInfo = "::4::5::OsConfigResource"; -}; - -instance of OMI_ConfigurationDocument -{ - Version="3.0.0"; - CompatibleVersionAdditionalProperties= {"Omi_BaseResource:ConfigurationName"}; - Author="Microsoft"; - GenerationDate="02/26/2025 13:48:30 UTC"; - Name="ComplianceExample"; -}; diff --git a/src/adapters/mc/complianceengine/example/CMakeLists.txt b/src/adapters/mc/complianceengine/example/CMakeLists.txt deleted file mode 100644 index 23b51d3137..0000000000 --- a/src/adapters/mc/complianceengine/example/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -project(OsConfigResourceComplianceExample) - -#add_custom_command( -# OUTPUT ComplianceExample.mof -# COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/MOFGenerator.py ${CMAKE_CURRENT_SOURCE_DIR}/ComplianceExample.json > ComplianceExample.mof -# DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/MOFGenerator.py ${CMAKE_CURRENT_SOURCE_DIR}/ComplianceExample.json -# VERBATIM) - -add_custom_target(stage_create_compliance_engine_example_zip - COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/Staging - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/ComplianceEngineExample.metaconfig.json" ${PROJECT_BINARY_DIR}/Staging/ -# COMMAND ${CMAKE_COMMAND} -E copy_if_different "${PROJECT_BINARY_DIR}/ComplianceEngineExample.mof" ${PROJECT_BINARY_DIR}/Staging/ - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/ComplianceEngineExample.mof" ${PROJECT_BINARY_DIR}/Staging/ - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ ${PROJECT_BINARY_DIR}/Staging/Modules/DscNativeResources/OsConfigResource/libOsConfigResource.so - DEPENDS OsConfigResourceComplianceEngine ComplianceEngineExample.mof) - -add_custom_target(create_compliance_engine_example_zip ALL - BYPRODUCTS ${OsConfigRootBinaryDir}/ComplianceEngineExample.zip - COMMAND ${CMAKE_COMMAND} -E tar "cfv" "${OsConfigRootBinaryDir}/ComplianceEngineExample.zip" --format=zip . - DEPENDS stage_create_compliance_engine_example_zip - WORKING_DIRECTORY ${PROJECT_BINARY_DIR}/Staging/) diff --git a/src/adapters/mc/complianceengine/example/ComplianceEngineExample.json b/src/adapters/mc/complianceengine/example/ComplianceEngineExample.json deleted file mode 100644 index 63a5aaa09f..0000000000 --- a/src/adapters/mc/complianceengine/example/ComplianceEngineExample.json +++ /dev/null @@ -1,88 +0,0 @@ -[ - { - "name": "Ensure NIS Client is not installed", - "key": "/cis/Ubuntu/24.04/v1.0.0/2/2/1", - "audit": { - "not": { - "PackageInstalled": { - "packageName": "nis" - } - } - } - }, - { - "name": "Ensure permissions on /etc/crontab are configured", - "key": "/cis/Ubuntu/24.04/v1.0.0/2/4/1/2", - "audit": { - "anyOf": [ - { - "not": { - "PackageInstalled": { - "packageName": "$CRON_PKG_NAME:cron" - } - } - }, - { - "EnsureFilePermissions": { - "filename": "/etc/crontab", - "owner": "root", - "group": "root", - "permissions": "600" - } - } - ] - }, - "remediate": { - "anyOf": [ - { - "not": { - "PackageInstalled": { - "packageName": "$CRON_PKG_NAME" - } - } - }, - { - "EnsureFilePermissions": { - "filename": "/etc/crontab", - "owner": "root", - "group": "root", - "permissions": "600" - } - } - ] - } - }, - { - "name": "Ensure permissions on /etc/cron.hourly are configured", - "key": "/cis/Ubuntu/22.04/v2.0.0/2/4/1/3", - "audit": { - "anyOf": [ - { - "not": { - "anyOf": [ - { - "PackageInstalled": { - "packageName": "$CRON_PKG_NAME:cron" - } - }, - { - "PackageInstalled": { - "packageName": "cronie" - } - } - ] - } - }, - { - "EnsureFilePermissions": { - "filename": "/etc/cron.hourly", - "owner": "root", - "group": "root", - "permissions": "600" - } - } - ] - } - } - -] diff --git a/src/adapters/mc/complianceengine/example/ComplianceEngineExample.metaconfig.json b/src/adapters/mc/complianceengine/example/ComplianceEngineExample.metaconfig.json deleted file mode 100644 index 397169cfdc..0000000000 --- a/src/adapters/mc/complianceengine/example/ComplianceEngineExample.metaconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Type": "AuditAndSet", - "Version": "1.0.0" -} diff --git a/src/adapters/mc/complianceengine/example/ComplianceEngineExample.mof b/src/adapters/mc/complianceengine/example/ComplianceEngineExample.mof deleted file mode 100644 index 8080454c3b..0000000000 --- a/src/adapters/mc/complianceengine/example/ComplianceEngineExample.mof +++ /dev/null @@ -1,62 +0,0 @@ -instance of OsConfigResource as $OsConfigResource0ref -{ - ResourceID = "Ensure NIS Client is not installed"; - PayloadKey = "/cis/ubuntu/22.04/v1.0.0/2/2/1"; - RuleId = "079122c1-4421-5be3-f897-92a1655e9228"; - ComponentName = "ComplianceEngine"; - ProcedureObjectName = "procedureEnsureNISClientIsNotInstalled"; - ProcedureObjectValue = "eyJuYW1lIjoiRW5zdXJlIE5JUyBDbGllbnQgaXMgbm90IGluc3RhbGxlZCIsImF1ZGl0Ijp7Im5vdCI6eyJQYWNrYWdlSW5zdGFsbGVkIjp7InBhY2thZ2VOYW1lIjoibmlzIn19fSwicGFyYW1ldGVycyI6e319"; - InitObjectName = "initEnsureNISClientIsNotInstalled"; - ReportedObjectName = "auditEnsureNISClientIsNotInstalled"; - ExpectedObjectValue = "PASS"; - DesiredObjectName = "remediateEnsureNISClientIsNotInstalled"; - DesiredObjectValue = ""; - ModuleName = "GuestConfiguration"; - ModuleVersion = "1.0.0"; - ConfigurationName = "ComplianceEngineExample"; - SourceInfo = "::4::5::OsConfigResource"; -}; -instance of OsConfigResource as $OsConfigResource1ref -{ - ResourceID = "Ensure permissions on /etc/crontab are configured"; - PayloadKey = "/cis/ubuntu/22.04/v1.0.0/2/4/1/2"; - RuleId = "65e74f75-0eac-58d0-1a5a-668ccb5c8f35"; - ComponentName = "ComplianceEngine"; - ProcedureObjectName = "procedureEnsurePermissionsOnEtcCrontabAreConfigured"; - ProcedureObjectValue = "eyJuYW1lIjoiRW5zdXJlIHBlcm1pc3Npb25zIG9uIC9ldGMvY3JvbnRhYiBhcmUgY29uZmlndXJlZCIsInJlbWVkaWF0ZSI6eyJhbnlPZiI6W3sibm90Ijp7IlBhY2thZ2VJbnN0YWxsZWQiOnsicGFja2FnZU5hbWUiOiIkQ1JPTl9QS0dfTkFNRSJ9fX0seyJFbnN1cmVGaWxlUGVybWlzc2lvbnMiOnsiZmlsZW5hbWUiOiIvZXRjL2Nyb250YWIiLCJvd25lciI6InJvb3QiLCJncm91cCI6InJvb3QiLCJwZXJtaXNzaW9ucyI6IjYwMCJ9fV19LCJhdWRpdCI6eyJhbnlPZiI6W3sibm90Ijp7IlBhY2thZ2VJbnN0YWxsZWQiOnsicGFja2FnZU5hbWUiOiIkQ1JPTl9QS0dfTkFNRSJ9fX0seyJFbnN1cmVGaWxlUGVybWlzc2lvbnMiOnsiZmlsZW5hbWUiOiIvZXRjL2Nyb250YWIiLCJvd25lciI6InJvb3QiLCJncm91cCI6InJvb3QiLCJwZXJtaXNzaW9ucyI6IjYwMCJ9fV19LCJwYXJhbWV0ZXJzIjp7IkNST05fUEtHX05BTUUiOiJjcm9uIn19"; - InitObjectName = "initEnsurePermissionsOnEtcCrontabAreConfigured"; - ReportedObjectName = "auditEnsurePermissionsOnEtcCrontabAreConfigured"; - ExpectedObjectValue = "PASS"; - DesiredObjectName = "remediateEnsurePermissionsOnEtcCrontabAreConfigured"; - DesiredObjectValue = "CRON_PKG_NAME=cron"; - ModuleName = "GuestConfiguration"; - ModuleVersion = "1.0.0"; - ConfigurationName = "ComplianceEngineExample"; - SourceInfo = "::4::5::OsConfigResource"; -}; -instance of OsConfigResource as $OsConfigResource2ref -{ - ResourceID = "Ensure permissions on /etc/cron.hourly are configured"; - PayloadKey = "/cis/ubuntu/22.04/v2.0.0/2/4/1/3"; - RuleId = "fb59b8f0-9cf2-19bc-decc-bde4c7f49c7d"; - ComponentName = "ComplianceEngine"; - ProcedureObjectName = "procedureEnsurePermissionsOnEtcCronHourlyAreConfigured"; - ProcedureObjectValue = "eyJuYW1lIjoiRW5zdXJlIHBlcm1pc3Npb25zIG9uIC9ldGMvY3Jvbi5ob3VybHkgYXJlIGNvbmZpZ3VyZWQiLCJhdWRpdCI6eyJhbnlPZiI6W3sibm90Ijp7ImFueU9mIjpbeyJQYWNrYWdlSW5zdGFsbGVkIjp7InBhY2thZ2VOYW1lIjoiJENST05fUEtHX05BTUUifX0seyJQYWNrYWdlSW5zdGFsbGVkIjp7InBhY2thZ2VOYW1lIjoiY3JvbmllIn19XX19LHsiRW5zdXJlRmlsZVBlcm1pc3Npb25zIjp7ImZpbGVuYW1lIjoiL2V0Yy9jcm9uLmhvdXJseSIsIm93bmVyIjoicm9vdCIsImdyb3VwIjoicm9vdCIsInBlcm1pc3Npb25zIjoiNjAwIn19XX0sInBhcmFtZXRlcnMiOnsiQ1JPTl9QS0dfTkFNRSI6ImNyb24ifX0="; - InitObjectName = "initEnsurePermissionsOnEtcCronHourlyAreConfigured"; - ReportedObjectName = "auditEnsurePermissionsOnEtcCronHourlyAreConfigured"; - ExpectedObjectValue = "PASS"; - DesiredObjectName = "remediateEnsurePermissionsOnEtcCronHourlyAreConfigured"; - DesiredObjectValue = "CRON_PKG_NAME=cron"; - ModuleName = "GuestConfiguration"; - ModuleVersion = "1.0.0"; - ConfigurationName = "ComplianceEngineExample"; - SourceInfo = "::4::5::OsConfigResource"; -}; -instance of OMI_ConfigurationDocument -{ - Version = "3.0.0"; - CompatibleVersionAdditionalProperties = {"Omi_BaseResource:ConfigurationName"}; - Author = "Microsoft"; - GenerationDate = "06/11/2025 19:53:38 UTC"; - Name = "ComplianceExample"; -}; diff --git a/src/adapters/mc/complianceengine/example/MOFGenerator.py b/src/adapters/mc/complianceengine/example/MOFGenerator.py deleted file mode 100644 index 8f5cff42ad..0000000000 --- a/src/adapters/mc/complianceengine/example/MOFGenerator.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -import base64 -import hashlib -import uuid -import sys -from datetime import datetime, timezone - -def generate_uuid(name): - return str(uuid.UUID(hashlib.sha256(name.encode()).hexdigest()[:32])) - -def compact_base64_encode(obj): - return base64.b64encode(json.dumps(obj, separators=(',', ':')).encode()).decode() - -def extract_parameters(audit): - parameters = {} - def recurse(obj): - if isinstance(obj, dict): - for key, value in obj.items(): - if isinstance(value, str) and value.startswith("$"): - param, _, default = value[1:].partition(":") - parameters[param] = default - obj[key] = f"${param}" - else: - recurse(value) - elif isinstance(obj, list): - for item in obj: - recurse(item) - recurse(audit) - return parameters - -def generate_osconfig_resource(resource, index): - resource_id = resource["name"] - payload_key = resource["key"] - rule_id = generate_uuid(resource_id) - - audit = resource.get("audit") - remediate = resource.get("remediate") - - parameters = extract_parameters(audit) - d = {"name": resource_id} - if remediate is not None: - d["remediate"] = remediate - if audit is not None: - d["audit"] = audit - if parameters is not None: - d["parameters"] = parameters - procedure_object_value = compact_base64_encode(d) - desired_object_value = " ".join([f"{param}={default}" for param, default in parameters.items()]) - - return f'''instance of OsConfigResource as $OsConfigResource{index}ref -{{ - ResourceID = "{resource_id}"; - PayloadKey = "{payload_key}"; - RuleId = "{rule_id}"; - ComponentName = "ComplianceEngine"; - ProcedureObjectName = "procedureObject"; - ProcedureObjectValue = "{procedure_object_value}"; - InitObjectName = "initObject"; - ReportedObjectName = "auditObject"; - ExpectedObjectValue = "PASS"; - DesiredObjectName = "remediateObject"; - DesiredObjectValue = "{desired_object_value}"; - ModuleName = "GuestConfiguration"; - ModuleVersion = "1.0.0"; - ConfigurationName = "ComplianceEngineExample"; - SourceInfo = "::4::5::OsConfigResource"; -}};''' - -def main(): - if len(sys.argv) != 2: - print("Usage: " + sys.argv[0] + " ") - sys.exit(1) - - input_file = sys.argv[1] - with open(input_file, 'r') as file: - resources = json.load(file) - - for index, resource in enumerate(resources): - print(generate_osconfig_resource(resource, index)) - - generation_date = datetime.now(timezone.utc).strftime("%m/%d/%Y %H:%M:%S %Z") - print(f'''instance of OMI_ConfigurationDocument -{{ - Version="3.0.0"; - CompatibleVersionAdditionalProperties= {{"Omi_BaseResource:ConfigurationName"}}; - Author="Microsoft"; - GenerationDate="{generation_date}"; - Name="ComplianceExample"; -}};''') - -if __name__ == "__main__": - main() diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index aa5bd71abe..c39f9d5a37 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -67,7 +67,6 @@ endif() add_module(securitybaseline) add_module(configuration) add_module(deviceinfo) -add_module(complianceengine) if (BUILD_MODULETEST) add_subdirectory(test) diff --git a/src/modules/complianceengine/CMakeLists.txt b/src/modules/complianceengine/CMakeLists.txt deleted file mode 100644 index b689075ee6..0000000000 --- a/src/modules/complianceengine/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -add_subdirectory(src) -if (BUILD_TESTS) - add_subdirectory(tests) -endif() diff --git a/src/modules/complianceengine/src/CMakeLists.txt b/src/modules/complianceengine/src/CMakeLists.txt deleted file mode 100644 index 8daf65d505..0000000000 --- a/src/modules/complianceengine/src/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -add_subdirectory(lib) -add_subdirectory(so) - -if (BUILD_COMPLIANCE_ENGINE_ASSESSOR) - add_subdirectory(assessor) - if(BUILD_TESTS) - add_subdirectory(lua-evaluator) - endif(BUILD_TESTS) -endif(BUILD_COMPLIANCE_ENGINE_ASSESSOR) diff --git a/src/modules/complianceengine/src/assessor/BenchmarkFormatter.cpp b/src/modules/complianceengine/src/assessor/BenchmarkFormatter.cpp deleted file mode 100644 index 1af24bc4d4..0000000000 --- a/src/modules/complianceengine/src/assessor/BenchmarkFormatter.cpp +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#include -#include -#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; - -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(DistributionInfo distributionInfo) - : mDistributionInfo(std::move(distributionInfo)) -{ -} - -Result BenchmarkFormatter::Begin(DistributionInfo distributionInfo, const Action action) -{ - BenchmarkFormatter formatter(std::move(distributionInfo)); - - auto json = JsonWrapper::MakeObject(); - if (!json.HasValue()) - { - return Error("Failed to initialize JSON object", ENOMEM); - } - - formatter.mJson = std::move(json.Value()); - auto* object = json_value_get_object(formatter.mJson.get()); - if (nullptr == object) - { - return Error("Failed to get JSON object", ENOMEM); - } - - formatter.mBegin = steady_clock::now(); - - 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); - } - - const auto arch = std::to_string(formatter.mDistributionInfo.architecture); - const auto distribution = std::to_string(formatter.mDistributionInfo.distribution); - auto* hostValue = json_value_init_object(); - if (nullptr == hostValue) - { - return Error("Failed to initialize host JSON object", ENOMEM); - } - auto* hostObject = json_value_get_object(hostValue); - if (nullptr == hostObject || JSONSuccess != json_object_set_string(hostObject, "arch", arch.c_str()) || - JSONSuccess != json_object_set_string(hostObject, "distribution", distribution.c_str()) || - JSONSuccess != json_object_set_string(hostObject, "distributionVersion", formatter.mDistributionInfo.version.c_str())) - { - json_value_free(hostValue); - return Error("Failed to set host info", ENOMEM); - } - if (JSONSuccess != json_object_set_value(object, "host", hostValue)) - { - json_value_free(hostValue); - return Error("Failed to set host info", ENOMEM); - } - - auto* arrayValue = json_value_init_array(); - if (nullptr == arrayValue) - { - return Error("Failed to initialize JSON array", ENOMEM); - } - if (JSONSuccess != json_object_set_value(object, "rules", arrayValue)) - { - json_value_free(arrayValue); - return Error("Failed to set rules", ENOMEM); - } - - return formatter; -} - -Optional BenchmarkFormatter::AddEntry(const MOF::Resource& entry, const Status status, const string& payload, - const std::map& parameters) & -{ - auto resultWrapper = JsonWrapper::MakeObject(); - if (!resultWrapper.HasValue()) - { - return Error("Failed to initialize JSON object", ENOMEM); - } - auto result = std::move(resultWrapper.Value()); - auto* object = json_value_get_object(result.get()); - if (nullptr == object) - { - return Error("Failed to get JSON object", ENOMEM); - } - - auto* indicatorsValue = json_parse_string(payload.c_str()); - if (nullptr == indicatorsValue) - { - return Error("Failed to parse JSON payload", ENOMEM); - } - if (json_value_get_type(indicatorsValue) != JSONArray) - { - json_value_free(indicatorsValue); - return Error("Invalid JSON payload", EINVAL); - } - - if (JSONSuccess != json_object_set_value(object, "indicators", indicatorsValue)) - { - json_value_free(indicatorsValue); - return Error("Failed to set JSON payload", ENOMEM); - } - - if (JSONSuccess != json_object_set_string(object, "title", entry.resourceID.c_str())) - { - return Error("Failed to set JSON title", ENOMEM); - } - - if (JSONSuccess != json_object_set_string(object, "ruleId", entry.ruleId.c_str())) - { - return Error("Failed to set JSON ruleId", ENOMEM); - } - - if (JSONSuccess != json_object_set_string(object, "section", entry.benchmarkInfo.section.c_str())) - { - return Error("Failed to set JSON section", 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", std::to_string(status).c_str())) - { - return Error("Failed to set JSON status", ENOMEM); - } - - // Surface the effective parameters (payload defaults merged with any user - // overrides) so renderers can show them without decoding the procedure blob. - auto* parametersValue = json_value_init_object(); - if (nullptr == parametersValue) - { - return Error("Failed to initialize parameters JSON object", ENOMEM); - } - auto* parametersObject = json_value_get_object(parametersValue); - if (nullptr == parametersObject) - { - json_value_free(parametersValue); - return Error("Failed to get parameters JSON object", ENOMEM); - } - for (const auto& parameter : parameters) - { - if (JSONSuccess != json_object_set_string(parametersObject, parameter.first.c_str(), parameter.second.c_str())) - { - json_value_free(parametersValue); - return Error("Failed to set parameter value", ENOMEM); - } - } - if (JSONSuccess != json_object_set_value(object, "parameters", parametersValue)) - { - json_value_free(parametersValue); - return Error("Failed to set parameters", 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 BenchmarkFormatter::Finish(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", std::to_string(status).c_str())) - { - 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/BenchmarkFormatter.hpp b/src/modules/complianceengine/src/assessor/BenchmarkFormatter.hpp deleted file mode 100644 index 39c6a37dd5..0000000000 --- a/src/modules/complianceengine/src/assessor/BenchmarkFormatter.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef COMPLIANCE_ENGINE_BENCHMARK_FORMATTER_HPP -#define COMPLIANCE_ENGINE_BENCHMARK_FORMATTER_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ComplianceEngine -{ -namespace BenchmarkFormatters -{ -// Formats a compliance scan run as a canonical JSON result document. Obtain an -// instance via Begin(), which initialises the result envelope and binds host -// provenance from the supplied DistributionInfo. Call AddEntry() for each -// evaluated rule and Finish() to obtain the serialised JSON. -class BenchmarkFormatter -{ -public: - static Result Begin(DistributionInfo distributionInfo, Action action); - - ~BenchmarkFormatter() = default; - BenchmarkFormatter(const BenchmarkFormatter&) = delete; - BenchmarkFormatter& operator=(const BenchmarkFormatter&) = delete; - BenchmarkFormatter(BenchmarkFormatter&&) = default; - BenchmarkFormatter& operator=(BenchmarkFormatter&&) = default; - - Optional AddEntry(const MOF::Resource& entry, Status status, const std::string& payload, const std::map& parameters) &; - Result Finish(Status status) &&; - -private: - static std::string ToISODatetime(const std::chrono::system_clock::time_point& tp); - explicit BenchmarkFormatter(DistributionInfo distributionInfo); - - std::chrono::time_point mBegin; - DistributionInfo mDistributionInfo; - JsonWrapper mJson; -}; -} // 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 deleted file mode 100644 index 25446a170e..0000000000 --- a/src/modules/complianceengine/src/assessor/CMakeLists.txt +++ /dev/null @@ -1,59 +0,0 @@ -# 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(lib_name compliance-engine-assessor-lib) - -# Library: parser + CLI options + formatters. Split out from the binary so -# unit tests can link the same object code as the shipping tool without -# rebuilding it or duplicating sources. -set(LIB_SOURCES - CliOptions.cpp - InputSecurity.cpp - Mof.cpp - BenchmarkFormatter.cpp - JUnitRenderer.cpp - TextRenderers.cpp -) - -add_library(${lib_name} STATIC ${LIB_SOURCES}) - -target_include_directories(${lib_name} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - ${MODULES_INC_DIR} - ) - -target_link_libraries(${lib_name} PUBLIC - logging - commonutils - parsonlib - complianceenginelib - ) - -add_executable(${target_name} Main.cpp) - -target_link_libraries(${target_name} - ${CMAKE_DL_LIBS} - ${lib_name} - ) - -include(GNUInstallDirs) -install(TARGETS ${target_name} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) - -if (BUILD_TESTS) - add_subdirectory(tests) -endif() - -if (BUILD_FUZZER) - add_subdirectory(fuzzer) -endif() diff --git a/src/modules/complianceengine/src/assessor/CliOptions.cpp b/src/modules/complianceengine/src/assessor/CliOptions.cpp deleted file mode 100644 index 8fb761faea..0000000000 --- a/src/modules/complianceengine/src/assessor/CliOptions.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include -#include -#include -#include -#include - -namespace ComplianceEngine -{ -namespace Assessor -{ - -using std::string; - -void PrintHelp(const std::string& programName) -{ - std::cout << "Usage: " + programName + " [options] [filename]\n\n"; - std::cout << "Commands:\n"; - std::cout << "\taudit\t\tEvaluate a benchmark and emit the canonical result JSON.\n"; - std::cout << "\tremediate\tRemediate a benchmark and emit the canonical result JSON.\n"; - std::cout << "\trender\t\tRender a canonical result JSON into a presentation format.\n"; - std::cout << "\n"; - std::cout << "Common options:\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 << "\n"; - std::cout << "audit / remediate options:\n"; - std::cout << "\t-e, --continue-on-error\tSkip rules that fail due to engine errors and continue processing. Returns 1 if any error occurred.\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 << "\tfilename\tProcess the specified MOF file. Optional: if skipped or the value is '-', the program reads standard input.\n"; - std::cout << "\n"; - std::cout << "render options:\n"; - std::cout << "\t-f, --format\tPresentation format. Allowed values: {junit, nested-list, compact-list, debug}. Default: junit.\n"; - std::cout << "\t --suite-name\tName for the JUnit . Default: compliance.\n"; - std::cout << "\tfilename\tRead the canonical result JSON from this file. Optional: if skipped or '-', reads standard input.\n"; -} - -// Long-only option identifiers (no short equivalent). Values start above the -// ASCII range so they never collide with a short-option character. -enum -{ - kSuiteNameOpt = 256 -}; - -// Command line parser using getopt_long. -// -// Resets getopt's global parser state on entry so this function can be safely -// called more than once per process (notably from unit tests). The shipping -// binary calls it exactly once, so the reset is a no-op there. -Result ParseCommandLine(const int argc, char* argv[]) -{ - optind = 0; -#ifdef optreset - optreset = 1; - optind = 1; -#endif - - const auto* short_opts = "hVvdel:s:f:"; - const option long_opts[] = {{"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"verbose", no_argument, nullptr, 'v'}, - {"debug", no_argument, nullptr, 'd'}, {"continue-on-error", no_argument, nullptr, 'e'}, {"log-file", required_argument, nullptr, 'l'}, - {"section", required_argument, nullptr, 's'}, {"format", required_argument, nullptr, 'f'}, - {"suite-name", required_argument, nullptr, kSuiteNameOpt}, {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 'e': - result.continueOnError = true; - break; - case 'l': - if (optarg[0] == '\0') - { - return Error("Log file path must not be empty."); - } - result.logFile = std::string(optarg); - break; - case 's': - if (optarg[0] == '\0') - { - return Error("Section must not be empty."); - } - result.section = std::string(optarg); - break; - case 'f': { - if (optarg[0] == '\0') - { - return Error("Format must not be empty."); - } - auto formatArg = std::string(optarg); - std::transform(formatArg.begin(), formatArg.end(), formatArg.begin(), [](unsigned char c) { return static_cast(::tolower(c)); }); - if (formatArg == "junit") - { - result.format = Format::Junit; - } - else if (formatArg == "nested-list") - { - result.format = Format::NestedList; - } - else if (formatArg == "compact-list") - { - result.format = Format::CompactList; - } - else if (formatArg == "debug") - { - result.format = Format::Debug; - } - else - { - return Error("Invalid format: " + formatArg + ". Allowed values: {junit, nested-list, compact-list, debug}."); - } - break; - } - case kSuiteNameOpt: - if (optarg[0] == '\0') - { - return Error("Suite name must not be empty."); - } - result.suiteName = std::string(optarg); - 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 if (arg == "render") - { - result.command = Command::Render; - } - else - { - return Error("Invalid command: '" + arg + "'. Must be 'audit', 'remediate' or 'render'."); - } - ++optind; - } - else - { - return Error("Missing required command: 'audit', 'remediate' or 'render'."); - } - - // 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."); - } - - // Cross-option validation: keep the audit/remediate surface (which always - // emits canonical JSON) free of presentation flags, and keep render free of - // scan flags. - if (Command::Render == result.command) - { - if (result.section.HasValue()) - { - return Error("--section is not valid for the 'render' subcommand."); - } - // Default the renderer when none was supplied. - if (!result.format.HasValue()) - { - result.format = Format::Junit; - } - } - else - { - if (result.format.HasValue()) - { - return Error("--format is only valid for the 'render' subcommand; 'audit' and 'remediate' always emit the canonical JSON."); - } - if (result.suiteName.HasValue()) - { - return Error("--suite-name is only valid for the 'render' subcommand."); - } - } - - return result; -} - -} // namespace Assessor -} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/CliOptions.hpp b/src/modules/complianceengine/src/assessor/CliOptions.hpp deleted file mode 100644 index 4427839ee2..0000000000 --- a/src/modules/complianceengine/src/assessor/CliOptions.hpp +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef COMPLIANCE_ENGINE_ASSESSOR_CLI_OPTIONS_HPP -#define COMPLIANCE_ENGINE_ASSESSOR_CLI_OPTIONS_HPP - -#include -#include -#include - -namespace ComplianceEngine -{ -namespace Assessor -{ - -enum class Command -{ - Help, - Version, - Audit, - Remediate, - Render -}; - -// Presentation formats produced by the `render` subcommand. `audit` / `remediate` -// no longer select a format: they always emit the canonical JSON, which `render` -// turns into one of these. -enum class Format -{ - NestedList, - CompactList, - Debug, - Junit -}; - -struct Options -{ - bool verbose = false; - bool debug = false; - bool continueOnError = false; - Optional logFile; - Optional format; - Command command = Command::Help; - std::string input; - Optional section; - // `render` only: the JUnit . The assessor does not know which - // benchmark package it came from, so the caller supplies this. - Optional suiteName; -}; - -void PrintHelp(const std::string& programName); - -Result ParseCommandLine(int argc, char* argv[]); - -} // namespace Assessor -} // namespace ComplianceEngine - -#endif // COMPLIANCE_ENGINE_ASSESSOR_CLI_OPTIONS_HPP diff --git a/src/modules/complianceengine/src/assessor/InputSecurity.cpp b/src/modules/complianceengine/src/assessor/InputSecurity.cpp deleted file mode 100644 index c5c67ec46c..0000000000 --- a/src/modules/complianceengine/src/assessor/InputSecurity.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#include -#include -#include -#include -#include -#include -#include -#include - -using ComplianceEngine::Error; -using ComplianceEngine::Result; - -namespace ComplianceEngine -{ -namespace Assessor -{ - -bool RefusePathTraversal(const std::string& path, OsConfigLogHandle logHandle) -{ - // Reject any path component equal to "..". We check for the four forms in - // which ".." can appear: as a prefix ("../"), embedded ("/../"), as a - // suffix ("/.." or the path is literally ".."), or the whole string. - if (path == ".." || path.compare(0, 3, "../") == 0 || path.find("/../") != std::string::npos || - (path.size() >= 3 && path.compare(path.size() - 3, 3, "/..") == 0)) - { - OsConfigLogError(logHandle, "Refusing input path '%s': contains path traversal sequence '..'.", path.c_str()); - return true; - } - return false; -} - -bool RefuseWritableParentDir(const std::string& path, OsConfigLogHandle logHandle) -{ - const size_t slash = path.rfind('/'); - std::string dir; - if (slash == std::string::npos) - { - dir = "."; - } - else if (slash == 0) - { - dir = "/"; - } - else - { - dir = path.substr(0, slash); - } - - struct stat st; - if (::stat(dir.c_str(), &st) != 0) - { - const std::string msg = std::strerror(errno); - OsConfigLogError(logHandle, "Refusing to use path '%s': failed to stat parent directory '%s': %s", path.c_str(), dir.c_str(), msg.c_str()); - return true; - } - if (st.st_uid != 0) - { - OsConfigLogError(logHandle, "Refusing to read input file '%s': parent directory '%s' not owned by root (uid %u).", path.c_str(), dir.c_str(), - static_cast(st.st_uid)); - return true; - } - if (st.st_mode & (S_IWGRP | S_IWOTH)) - { - OsConfigLogError(logHandle, "Refusing to read input file '%s': parent directory '%s' writable by group or others (mode %04o).", path.c_str(), - dir.c_str(), static_cast(st.st_mode & 07777)); - return true; - } - return false; -} - -Result OpenVerifiedInput(const std::string& path, OsConfigLogHandle logHandle) -{ - // O_NONBLOCK prevents open() from blocking on a FIFO: without it, - // O_RDONLY on a FIFO stalls until a writer appears, which would hang the - // assessor indefinitely. For regular files O_NONBLOCK has no effect on - // Linux (reads always complete immediately). Non-regular files are rejected - // by the S_ISREG fstat check below once the fd is in hand. - const int fd = ::open(path.c_str(), O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); - if (fd < 0) - { - if (errno == ELOOP) - { - OsConfigLogError(logHandle, "Refusing to open input file '%s': path is a symlink.", path.c_str()); - return Error("path is a symlink", ELOOP); - } - const int savedErrno = errno; - const std::string msg = std::strerror(savedErrno); - OsConfigLogError(logHandle, "Failed to open input file '%s': %s", path.c_str(), msg.c_str()); - return Error(msg, savedErrno); - } - - struct stat st; - if (::fstat(fd, &st) != 0) - { - const int savedErrno = errno; - const std::string msg = std::strerror(savedErrno); - OsConfigLogError(logHandle, "Failed to stat input file '%s': %s", path.c_str(), msg.c_str()); - ::close(fd); - return Error(msg, savedErrno); - } - if (!S_ISREG(st.st_mode)) - { - // Refuse FIFOs, devices, sockets, and directories. A FIFO would let an - // attacker block the read indefinitely or stream unbounded data; a - // character device such as /dev/zero would do the same. Streaming - // inputs (pipes, process substitution) must be supplied via stdin, - // which deliberately bypasses these on-disk integrity checks. - OsConfigLogError(logHandle, "Refusing to read input file '%s': not a regular file.", path.c_str()); - ::close(fd); - return Error("not a regular file", ENOTSUP); - } - - // O_NONBLOCK was set solely to prevent open() from blocking on a FIFO. - // Now that fstat() has confirmed this is a regular file, clear the flag so - // that subsequent reads have straightforward blocking semantics and the - // caller's read loop does not need to handle EAGAIN/EWOULDBLOCK. - { - const int flags = ::fcntl(fd, F_GETFL); - if (flags < 0 || ::fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) - { - const int savedErrno = errno; - const std::string msg = std::strerror(savedErrno); - OsConfigLogError(logHandle, "Failed to clear O_NONBLOCK on input file '%s': %s", path.c_str(), msg.c_str()); - ::close(fd); - return Error(msg, savedErrno); - } - } - if (st.st_uid != 0) - { - OsConfigLogError(logHandle, "Refusing to read input file '%s': not owned by root (uid %u).", path.c_str(), static_cast(st.st_uid)); - ::close(fd); - return Error("not owned by root", EPERM); - } - if (st.st_mode & (S_IWGRP | S_IWOTH)) - { - OsConfigLogError(logHandle, "Refusing to read input file '%s': writable by group or others (mode %04o).", path.c_str(), - static_cast(st.st_mode & 07777)); - ::close(fd); - return Error("writable by group or others", EPERM); - } - return fd; -} - -bool RefuseUnsafeLogFile(const std::string& path, OsConfigLogHandle logHandle) -{ - // The log file is opened (and chmod'd) by the shared logging code while we - // run as root, and that open follows symlinks. Apply the same posture as - // --input so an attacker cannot redirect root's writes: - // - reject path traversal, - // - require a root-owned, non-group/world-writable parent directory - // (prevents a rename-swap onto a hostile target), - // - if the path already exists, reject symlinks, non-root ownership, or - // group/world-writable modes. A non-existent path is fine because it - // will be created inside the parent directory we just validated. - if (RefusePathTraversal(path, logHandle)) - { - return true; - } - if (RefuseWritableParentDir(path, logHandle)) - { - return true; - } - - struct stat st; - if (::lstat(path.c_str(), &st) != 0) - { - if (errno == ENOENT) - { - // Does not exist yet; it will be created in the parent directory already verified to be root-owned and safe. - return false; - } - const std::string msg = std::strerror(errno); - OsConfigLogError(logHandle, "Refusing to use log file '%s': failed to inspect path: %s", path.c_str(), msg.c_str()); - return true; - } - if (S_ISLNK(st.st_mode)) - { - OsConfigLogError(logHandle, "Refusing to use log file '%s': path is a symlink.", path.c_str()); - return true; - } - if (!S_ISREG(st.st_mode)) - { - OsConfigLogError(logHandle, "Refusing to use log file '%s': not a regular file.", path.c_str()); - return true; - } - if (st.st_uid != 0) - { - OsConfigLogError(logHandle, "Refusing to use log file '%s': not owned by root (uid %u).", path.c_str(), static_cast(st.st_uid)); - return true; - } - if (st.st_mode & (S_IWGRP | S_IWOTH)) - { - OsConfigLogError(logHandle, "Refusing to use log file '%s': writable by group or others (mode %04o).", path.c_str(), - static_cast(st.st_mode & 07777)); - return true; - } - return false; -} - -} // namespace Assessor -} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/InputSecurity.hpp b/src/modules/complianceengine/src/assessor/InputSecurity.hpp deleted file mode 100644 index 646d2680bb..0000000000 --- a/src/modules/complianceengine/src/assessor/InputSecurity.hpp +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#ifndef COMPLIANCE_ENGINE_ASSESSOR_INPUT_SECURITY_HPP -#define COMPLIANCE_ENGINE_ASSESSOR_INPUT_SECURITY_HPP - -#include -#include -#include - -namespace ComplianceEngine -{ -namespace Assessor -{ - -// Returns true (and logs an error) if the parent directory of `path` is not -// owned by root or is writable by group/others. A writable parent directory -// enables a rename-swap attack: an attacker can unlink the file after it has -// been validated and replace it with a hostile one before it is read. -// Uses stat() (follows symlinks) so intermediate directory symlinks do not -// defeat the check. -bool RefuseWritableParentDir(const std::string& path, OsConfigLogHandle logHandle); - -// Opens `path` safely for reading, refusing symlinks and verifying ownership -// and permissions via fstat() on the resulting fd. -// -// Security properties: -// - O_NOFOLLOW causes the kernel to refuse a symlink in the final path -// component atomically (returns ELOOP), eliminating the lstat-then-open -// TOCTOU window. -// - O_NONBLOCK prevents the open() call from blocking on a FIFO (without -// it, O_RDONLY on a FIFO stalls until a writer appears). After fstat() -// confirms the file is a regular file, O_NONBLOCK is cleared via fcntl() -// so that subsequent reads have straightforward blocking semantics and the -// caller need not handle EAGAIN/EWOULDBLOCK. -// - fstat() checks the inode we actually hold, not a potentially-swapped -// path entry. The file must be a regular file, root-owned, and not -// group/world-writable. Non-regular files (FIFOs, devices) are refused so -// they cannot block the read or stream unbounded data; streaming inputs -// must be supplied via stdin instead. -// - O_CLOEXEC prevents accidental fd inheritance into child processes. -// -// Returns the open fd on success (caller owns it and must ::close() it), -// or an Error on any failure (error already logged). -ComplianceEngine::Result OpenVerifiedInput(const std::string& path, OsConfigLogHandle logHandle); - -// Rejects path traversal sequences ("/../", "/.." suffix, "../" prefix, or -// a bare ".."). These sequences can be used to escape an expected directory -// even when all other permission checks pass. -// Returns true (and logs an error) if the path contains traversal components. -bool RefusePathTraversal(const std::string& path, OsConfigLogHandle logHandle); - -// Returns true (and logs an error) if the log-file `path` is unsafe to open -// while running as root. The shared logging code opens the log with a -// symlink-following append and then chmod's it, so an attacker-controlled -// symlink or a writable parent directory could redirect root's writes onto a -// sensitive file. This applies the same posture as OpenVerifiedInput: rejects -// path traversal, requires a root-owned non-writable parent directory, and (if -// the path already exists) rejects symlinks, non-regular files, non-root -// ownership, and group/world-writable modes. A non-existent path is allowed -// because it is created inside the validated parent directory. -// -// Known limitation (TOCTOU): unlike OpenVerifiedInput, this cannot fstat() a -// held fd, because the shared OpenLog() API is path-only and TrimLog() -// re-opens the path on every rotation. The check is therefore an lstat() of -// the path shortly before OpenLog() (and each later rotation) re-resolves it -// with symlink-following fopen(), leaving a small check-to-use window. The -// required root-owned, non-writable parent directory closes that window in -// practice by preventing any swap of the entry. See the threat-model comment -// in Main.cpp for details. -bool RefuseUnsafeLogFile(const std::string& path, OsConfigLogHandle logHandle); - -} // namespace Assessor -} // namespace ComplianceEngine - -#endif // COMPLIANCE_ENGINE_ASSESSOR_INPUT_SECURITY_HPP diff --git a/src/modules/complianceengine/src/assessor/JUnitRenderer.cpp b/src/modules/complianceengine/src/assessor/JUnitRenderer.cpp deleted file mode 100644 index be226dd957..0000000000 --- a/src/modules/complianceengine/src/assessor/JUnitRenderer.cpp +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#include -#include -#include -#include -#include -#include - -namespace ComplianceEngine -{ -namespace Assessor -{ -using std::string; - -namespace -{ -// Escapes the five XML entities and neutralises control characters that are -// illegal in XML 1.0 text (everything below 0x20 except tab/newline/carriage -// return), so arbitrary indicator/parameter text cannot produce malformed XML. -string EscapeXml(const string& in) -{ - string out; - out.reserve(in.size()); - for (const char ch : in) - { - switch (ch) - { - case '&': - out += "&"; - break; - case '<': - out += "<"; - break; - case '>': - out += ">"; - break; - case '"': - out += """; - break; - case '\'': - out += "'"; - break; - default: - if (static_cast(ch) < 0x20 && ch != '\t' && ch != '\n' && ch != '\r') - { - out += ' '; - } - else - { - out += ch; - } - break; - } - } - return out; -} - -// Recursively renders an indicators array into the readable indented style used -// by tests/reporting/junit.py: "{indent*depth} - {label} [{status}]". A node's -// label is its message (leaf) or its procedure (branch); children are rendered -// one level deeper. -// -// Recursion is bounded to guard against pathologically deep (or maliciously -// crafted) indicator trees causing stack exhaustion; nodes below the limit are -// silently dropped from the rendered output. -constexpr size_t cMaxIndicatorDepth = 16; - -void AppendIndicators(const JSON_Array* indicators, size_t depth, std::ostringstream& body) -{ - if (nullptr == indicators || depth >= cMaxIndicatorDepth) - { - return; - } - const size_t count = json_array_get_count(indicators); - for (size_t i = 0; i < count; ++i) - { - const JSON_Object* node = json_array_get_object(indicators, i); - if (nullptr == node) - { - continue; - } - string label = StringOrEmpty(json_object_get_string(node, "message")); - if (label.empty()) - { - label = StringOrEmpty(json_object_get_string(node, "procedure")); - } - const string status = StringOrEmpty(json_object_get_string(node, "status")); - body << string(depth * 2, ' ') << " - " << label; - if (!status.empty()) - { - body << " [" << status << "]"; - } - body << "\n"; - AppendIndicators(json_object_get_array(node, "indicators"), depth + 1, body); - } -} - -// Builds the human-readable failure body for a rule: a Parameters section -// (present when the canonical JSON carries per-rule parameters) followed by an -// indented Indicators tree. -string BuildBody(const JSON_Object* rule) -{ - std::ostringstream body; - - const JSON_Object* parameters = json_object_get_object(rule, "parameters"); - body << "Parameters:\n"; - if (nullptr != parameters) - { - const size_t count = json_object_get_count(parameters); - for (size_t i = 0; i < count; ++i) - { - const string key = StringOrEmpty(json_object_get_name(parameters, i)); - const JSON_Value* value = json_object_get_value_at(parameters, i); - string valueStr = StringOrEmpty(json_value_get_string(value)); - if (valueStr.empty() && nullptr != value && json_value_get_type(value) != JSONString) - { - char* serialized = json_serialize_to_string(value); - if (nullptr != serialized) - { - // Deliberately deep-copy into our own std::string before - // freeing parson's buffer, so the value survives the free. - valueStr = std::string(serialized); - json_free_serialized_string(serialized); - } - } - body << " - " << key << ": " << valueStr << "\n"; - } - } - - body << "\nIndicators:\n"; - AppendIndicators(json_object_get_array(rule, "indicators"), 0, body); - return body.str(); -} -} // anonymous namespace - -Result RenderJUnit(const string& canonicalJson, const string& suiteName) -{ - JSON_Value* root = json_parse_string(canonicalJson.c_str()); - if (nullptr == root) - { - return Error("Failed to parse canonical result JSON", EINVAL); - } - // Own the parsed document for the duration of this function. - struct RootGuard - { - JSON_Value* v; - ~RootGuard() - { - json_value_free(v); - } - } guard{root}; - - const JSON_Object* rootObject = json_value_get_object(root); - if (nullptr == rootObject) - { - return Error("Canonical result JSON is not an object", EINVAL); - } - const JSON_Array* rules = json_object_get_array(rootObject, "rules"); - if (nullptr == rules) - { - return Error("Canonical result JSON has no 'rules' array", EINVAL); - } - - const size_t ruleCount = json_array_get_count(rules); - size_t failureCount = 0; - size_t skippedCount = 0; - std::ostringstream cases; - for (size_t i = 0; i < ruleCount; ++i) - { - const JSON_Object* rule = json_array_get_object(rules, i); - if (nullptr == rule) - { - return Error("Canonical result JSON 'rules' entry is not an object", EINVAL); - } - const string section = StringOrEmpty(json_object_get_string(rule, "section")); - const string ruleName = StringOrEmpty(json_object_get_string(rule, "ruleName")); - const string status = StringOrEmpty(json_object_get_string(rule, "status")); - - // Guard against schema drift / upstream bugs: an unrecognised or missing - // status must not be silently rendered as a passing test case. - if (status != "Compliant" && status != "NonCompliant" && status != "NotApplicable") - { - return Error("Canonical result JSON rule has invalid 'status' value: '" + status + "'", EINVAL); - } - - cases << " \n"; - cases << " " << EscapeXml(BuildBody(rule)) << "\n"; - cases << " \n"; - } - else if (status == "NotApplicable") - { - // A not-applicable rule is neither a pass nor a failure; JUnit models - // this as a skipped test case. - ++skippedCount; - cases << ">\n"; - cases << " " << EscapeXml(BuildBody(rule)) << "\n"; - cases << " \n"; - } - else - { - // status == "Compliant": a bare passing test case. - cases << "/>\n"; - } - } - - std::ostringstream out; - out << "\n"; - out << "\n"; - out << " \n"; - out << cases.str(); - out << " \n"; - out << "\n"; - return out.str(); -} - -} // namespace Assessor -} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/JUnitRenderer.hpp b/src/modules/complianceengine/src/assessor/JUnitRenderer.hpp deleted file mode 100644 index 2ed9d6a7e4..0000000000 --- a/src/modules/complianceengine/src/assessor/JUnitRenderer.hpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#ifndef COMPLIANCE_ENGINE_ASSESSOR_JUNIT_RENDERER_HPP -#define COMPLIANCE_ENGINE_ASSESSOR_JUNIT_RENDERER_HPP - -#include -#include - -namespace ComplianceEngine -{ -namespace Assessor -{ -// Renders a canonical assessor result JSON (as emitted by `audit` / `remediate`) -// into a JUnit XML document. -// -// - one name=> per rule, -// - a only for NonCompliant rules (Compliant rules are bare -// passing ), -// - the failure body carries the rule's Parameters and Indicators, modelled on -// the augmentation engine's tests/reporting/junit.py. -// -// `section` is used verbatim as the classname; it is framework-agnostic (a -// dotted CIS number or a STIG id), so the renderer makes no CIS-specific -// assumptions. `suiteName` names the ; the assessor does not know -// which benchmark package it came from, so the caller supplies it. -Result RenderJUnit(const std::string& canonicalJson, const std::string& suiteName); - -} // namespace Assessor -} // namespace ComplianceEngine - -#endif // COMPLIANCE_ENGINE_ASSESSOR_JUNIT_RENDERER_HPP diff --git a/src/modules/complianceengine/src/assessor/Main.cpp b/src/modules/complianceengine/src/assessor/Main.cpp deleted file mode 100644 index 2813adcd51..0000000000 --- a/src/modules/complianceengine/src/assessor/Main.cpp +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// compliance-engine-assessor -// -// Threat model -// ------------ -// This tool is intended to run as root on Linux endpoints to perform CIS -// benchmark audit and remediation. The trust boundary is the invoking -// operator: the input MOF file, the log-file path, and command-line arguments -// are treated as operator-supplied (trusted to be benign in intent, but not -// to be free of bugs or accidental hostile content). -// -// - The input MOF parser is strict and streaming (see MofResourceRange): it -// validates the fixed field set the augmentation engine emits, rejects -// unknown fields, and bounds line length, total size, and entry count. It -// owns the input file and encapsulates the integrity checks below. A fuzzer -// target exercises it; extend the corpus when changing the format. -// -// - Input file integrity (when --input is used; stdin bypasses all checks): -// -// 1. Parent directory (stat): must be root-owned and not writable by -// group or others. A writable directory enables a rename-swap attack: -// an attacker can unlink the validated file and place a hostile one -// before the process reads it. -// -// 2. open(O_RDONLY|O_NOFOLLOW|O_CLOEXEC): the kernel refuses symlinks in -// the final path component atomically (ELOOP), eliminating the -// lstat-then-open TOCTOU window. Symlinks are intentionally rejected -// rather than accepted-with-a-warning; callers that stage input via a -// symlink must resolve the link before passing the path. Note: symlinks -// in intermediate path components are not checked; the operator is -// trusted to supply a straightforward path. -// -// 3. fstat on the open fd: ownership and mode are verified against the -// inode we actually hold, not a potentially-swapped path entry. The -// file must be a regular file (FIFOs, devices, sockets are refused so -// they cannot block the read or stream unbounded data), root-owned, and -// not group/world-writable. -// -// 4. The verified fd is wrapped in a stream and read incrementally (never -// slurped whole). The fd keeps the inode reachable across the read even -// if the directory entry is concurrently renamed or unlinked. The total -// bytes read, per-line length, and entry count are all bounded inside the -// streaming parser to keep memory use bounded. -// -// - stdin (--input not supplied): all file integrity checks are bypassed. -// Streaming inputs (pipes, process substitution) must use stdin. The bytes -// consumed, per-line length, and total entry count are still bounded inside -// the streaming MOF parser. Callers in automated pipelines should always use -// --input with a root-owned, non-world-writable file. -// -// - umask is tightened to at least S_IRWXG|S_IRWXO (preserving any stricter -// inherited mask). The log file when --log-file is supplied is the primary -// case. -// -// - The --log-file path is validated before opening (RefuseUnsafeLogFile): -// the shared logging code opens it with a symlink-following append and -// chmod's it while we run as root, so a symlink, non-root-owned target, or -// writable parent directory is refused to prevent redirecting root's writes -// onto a sensitive file. -// -// Residual TOCTOU (known limitation): unlike --input, the log file is NOT -// verified via fstat() on a held fd. The shared OpenLog() API is path-only -// (no fd-accepting entry point) and TrimLog() re-opens the path with -// fopen() on every log rotation, so a pinned, pre-verified fd cannot be -// handed to the logging layer; both the initial open and each rotation -// re-resolve the path with symlink-following fopen(). RefuseUnsafeLogFile() -// therefore checks the path with lstat() shortly before OpenLog() resolves -// it again, leaving a small check-to-use window. That window is closed in -// practice by the parent-directory check: requiring the parent to be -// root-owned and not group/world-writable prevents an attacker from -// creating, renaming, or swapping the entry at all, so the path cannot be -// pointed at a new target between the check and any (re-)open. Fully -// eliminating the window (fd-based open with O_NOFOLLOW handed to the -// logger) would require changing the shared logging library, which is -// out of scope here as it affects every azure-osconfig binary. -// -// - The PATH/IFS environment is inherited and used by procedure scripts the -// engine spawns. Sanitizing the environment is the engine's -// responsibility, not the assessor's. - -#include "BenchmarkFormatter.hpp" -#include "CliOptions.hpp" -#include "InputSecurity.hpp" -#include "JUnitRenderer.hpp" -#include "Mof.hpp" -#include "TextRenderers.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using ComplianceEngine::Action; -using ComplianceEngine::AssessorContext; -using ComplianceEngine::CombineAllOf; -using ComplianceEngine::DistributionInfo; -using ComplianceEngine::Engine; -using ComplianceEngine::Error; -using ComplianceEngine::Optional; -using ComplianceEngine::PayloadFormatter; -using ComplianceEngine::Result; -using ComplianceEngine::Status; -using ComplianceEngine::Assessor::Command; -using ComplianceEngine::Assessor::Format; -using ComplianceEngine::Assessor::Options; -using ComplianceEngine::Assessor::ParseCommandLine; -using ComplianceEngine::Assessor::PrintHelp; -using ComplianceEngine::Assessor::RefuseUnsafeLogFile; -using ComplianceEngine::Assessor::RenderJUnit; -using ComplianceEngine::Assessor::RenderText; -using ComplianceEngine::Assessor::TextStyle; -using ComplianceEngine::BenchmarkFormatters::BenchmarkFormatter; -using ComplianceEngine::MOF::MofResourceRange; -using std::string; - -namespace -{ -// Upper bound on a canonical result JSON fed to `render`. Generous (results for -// a full benchmark are well under this) but bounds memory for a hostile input. -constexpr std::size_t kMaxResultJsonBytes = static_cast(256) * 1024 * 1024; - -// Reads an entire stream into a string, refusing inputs larger than the cap. -Result ReadAllBounded(std::istream& stream, std::size_t cap) -{ - string content; - char buffer[64 * 1024]; - while (stream.read(buffer, sizeof(buffer)) || stream.gcount() > 0) - { - content.append(buffer, static_cast(stream.gcount())); - if (content.size() > cap) - { - return Error("Input exceeds the maximum allowed size", EFBIG); - } - } - if (stream.bad()) - { - return Error("Failed to read input", EIO); - } - return content; -} - -// Renders a canonical result JSON (read from stdin or a file) into the format -// selected on the `render` subcommand. Runs without root and touches no system -// state, so it needs none of the MOF input hardening `audit`/`remediate` apply. -int RunRender(const Options& options) -{ - Result jsonResult = Error("uninitialized"); - if (options.input.empty() || options.input == "-") - { - jsonResult = ReadAllBounded(std::cin, kMaxResultJsonBytes); - } - else - { - std::ifstream file(options.input, std::ios::binary); - if (!file.is_open()) - { - std::cerr << "Error: failed to open input file '" << options.input << "'." << std::endl; - return 1; - } - jsonResult = ReadAllBounded(file, kMaxResultJsonBytes); - } - if (!jsonResult.HasValue()) - { - std::cerr << "Error: " << jsonResult.Error().message << std::endl; - return 1; - } - - const string suiteName = options.suiteName.HasValue() ? options.suiteName.Value() : string("compliance"); - - // The parser defaults the format to Junit when none is supplied. - const Format format = options.format.HasValue() ? options.format.Value() : Format::Junit; - Result rendered = Error("uninitialized"); - switch (format) - { - case Format::Junit: - rendered = RenderJUnit(jsonResult.Value(), suiteName); - break; - case Format::NestedList: - rendered = RenderText(jsonResult.Value(), TextStyle::NestedList); - break; - case Format::CompactList: - rendered = RenderText(jsonResult.Value(), TextStyle::CompactList); - break; - case Format::Debug: - rendered = RenderText(jsonResult.Value(), TextStyle::Debug); - break; - } - if (!rendered.HasValue()) - { - std::cerr << "Error: " << rendered.Error().message << std::endl; - return 1; - } - std::cout << rendered.Value(); - return 0; -} -} // anonymous namespace - -int main(int argc, char* argv[]) -{ - // Ensure file-creation permissions are at least as restrictive as 0077 - // without overriding a stricter inherited mask. - ::umask(::umask(0) | S_IRWXG | S_IRWXO); - - 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; - } - - // `render` is a pure, root-free transformation of a canonical result JSON; - // it needs neither the engine nor the MOF input path, so dispatch it early. - if (Command::Render == options.command) - { - return RunRender(options); - } - - // Validate the log-file path before opening it. The shared logging code - // opens the log with a symlink-following append and chmod's it while we run - // as root, so an attacker-controlled symlink or writable parent directory - // could redirect those writes. No log handle exists yet, so failures are - // reported to stderr. - if (options.logFile.HasValue()) - { - if (options.logFile->empty() || RefuseUnsafeLogFile(options.logFile.Value(), nullptr)) - { - std::cerr << "Error: refusing to use unsafe log file path." << std::endl; - return 1; - } - } - - std::unique_ptr logHandle(options.logFile.HasValue() ? OpenLog(options.logFile->c_str(), nullptr) : nullptr, - [](OsConfigLog* h) { - OsConfigLogHandle tmp = h; - CloseLog(&tmp); - }); - if (logHandle) - { - SetConsoleLoggingEnabled(false); - } - - if (options.verbose) - { - SetLoggingLevel(LoggingLevel::LoggingLevelInformational); - OsConfigLogInfo(logHandle.get(), "Verbose logging enabled"); - } - - if (options.debug) - { - SetLoggingLevel(LoggingLevel::LoggingLevelDebug); - OsConfigLogInfo(logHandle.get(), "Debug logging enabled"); - } - - auto context = std::unique_ptr(new AssessorContext(logHandle.get())); - // The Engine takes ownership of a PayloadFormatter and uses it polymorphically - // to render each rule's indicators. Pass the JSON one explicitly: the - // constructor's default is a DebugFormatter, whose text output could not be - // embedded as the canonical result's indicators array. - Engine engine(std::move(context), std::unique_ptr(new ComplianceEngine::JsonFormatter())); - - // Determine the OS this tool is running on so rules that target a different - // distribution/version can be skipped. LoadDistributionInfo prefers the - // operator-supplied override file and falls back to /etc/os-release. If the - // OS cannot be identified (e.g. an unmapped distribution ID and no override - // file), abort rather than silently running rules meant for another system. - auto distributionInfoError = engine.LoadDistributionInfo(); - if (distributionInfoError) - { - OsConfigLogError(logHandle.get(), "Failed to determine system distribution: %s", distributionInfoError.Value().message.c_str()); - OsConfigLogError(logHandle.get(), "To specify the OS identity explicitly, place an override in the '%s' file", DistributionInfo::cDefaultOverrideFilePath); - return 1; - } - - // `audit` / `remediate` always emit the canonical JSON. The benchmark - // formatter builds the result envelope; the engine is separately given a - // JSON payload formatter (at its construction, above) to render each rule's - // indicators. Presentation is the `render` subcommand's job. - const auto& distributionInfo = engine.GetDistributionInfo().Value(); - auto formatterResult = BenchmarkFormatter::Begin(distributionInfo, options.command == Command::Audit ? Action::Audit : Action::Remediate); - if (!formatterResult.HasValue()) - { - OsConfigLogError(logHandle.get(), "Failed to begin formatted output: %s", formatterResult.Error().message.c_str()); - return 1; - } - auto& benchmarkFormatter = formatterResult.Value(); - - // Open the input as a strictly-validated, streaming MOF range. For --input - // the range encapsulates the full input-hardening posture (path-traversal - // rejection, root-owned non-writable parent directory, O_NOFOLLOW open, and - // regular-file/ownership/mode checks) and owns the file; for stdin it - // streams without those on-disk checks. Size, line, and entry caps are - // enforced inside the range. - auto rangeResult = options.input.empty() ? MofResourceRange::Make(std::cin, logHandle.get()) : MofResourceRange::Make(options.input, logHandle.get()); - if (!rangeResult.HasValue()) - { - OsConfigLogError(logHandle.get(), "Failed to open MOF input: %s", rangeResult.Error().message.c_str()); - return 1; - } - auto& mofRange = rangeResult.Value(); - - auto status = Status::Compliant; - bool hasError = false; - for (const auto& entryResult : mofRange) - { - if (!entryResult.HasValue()) - { - OsConfigLogError(logHandle.get(), "Failed to parse MOF entry: %s", entryResult.Error().message.c_str()); - return 1; - } - - const auto& mofEntry = entryResult.Value(); - - // Abort as soon as we encounter a rule that does not target the detected - // distribution/version. This mirrors ComplianceEngineCheckApplicability - // in the module interface: the benchmark's distribution must match and - // its version glob must match the running system's VERSION_ID. Every - // entry in a MOF belongs to the same benchmark, so a single mismatch - // means the whole MOF targets another system (or this system was - // misdetected); running any of its rules would report spurious results. - const auto& distributionInfo = engine.GetDistributionInfo().Value(); - if (!mofEntry.benchmarkInfo.Match(distributionInfo)) - { - OsConfigLogError(logHandle.get(), "Aborting on entry %s: benchmark is not applicable for the current distribution", mofEntry.resourceID.c_str()); - OsConfigLogError(logHandle.get(), "Current system identification: %s", std::to_string(distributionInfo).c_str()); - auto overridden = distributionInfo; - overridden.distribution = mofEntry.benchmarkInfo.distribution; - overridden.version = mofEntry.benchmarkInfo.SanitizedVersion(); - OsConfigLogError(logHandle.get(), "To override this detection, place the following line inside the '%s' file: %s", - DistributionInfo::cDefaultOverrideFilePath, std::to_string(overridden).c_str()); - return 1; - } - - if (options.section.HasValue()) - { - if (mofEntry.benchmarkInfo.section.find(options.section.Value()) != 0) - { - OsConfigLogDebug(logHandle.get(), "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.get(), "Failed to set procedure: %s", procedureResult.Error().message.c_str()); - if (!options.continueOnError) - { - return 1; - } - hasError = true; - continue; - } - - switch (options.command) - { - case Command::Audit: { - if (mofEntry.hasInitAudit) - { - // If the producer flagged InitObject support but supplied no - // DesiredObjectValue, fall back to an empty JSON object so - // we don't deref an empty Optional. - const string initPayload = mofEntry.payload.HasValue() ? mofEntry.payload.Value() : string("{}"); - auto result = engine.MmiSet((string("init") + mofEntry.ruleName).c_str(), initPayload); - if (!result.HasValue()) - { - OsConfigLogError(logHandle.get(), "Failed to init audit: %s", result.Error().message.c_str()); - if (!options.continueOnError) - { - return 1; - } - hasError = true; - continue; - } - } - - auto ruleName = string("audit") + mofEntry.ruleName; - auto result = engine.MmiGet(ruleName.c_str()); - if (!result.HasValue()) - { - OsConfigLogError(logHandle.get(), "Failed to perform audit: %s", result.Error().message.c_str()); - if (!options.continueOnError) - { - return 1; - } - hasError = true; - continue; - } - - auto error = benchmarkFormatter.AddEntry(mofEntry, result.Value().status, result.Value().payload, engine.GetParameters(mofEntry.ruleName)); - if (error) - { - OsConfigLogError(logHandle.get(), "Failed to add entry to JSON formatter: %s", error.Value().message.c_str()); - if (!options.continueOnError) - { - return 1; - } - hasError = true; - continue; - } - - // Aggregate the overall benchmark status the same way the engine - // aggregates an allOf (CombineAllOf): NonCompliant dominates, - // NotApplicable is sticky, otherwise Compliant. - status = CombineAllOf(status, result.Value().status); - - break; - } - - case Command::Remediate: { - // The augmentation engine emits an empty DesiredObjectValue for - // every rule (modelled here as an absent payload); fall back to - // an empty JSON object so remediation can still run, mirroring - // the audit-init path above. - const string remediatePayload = mofEntry.payload.HasValue() ? mofEntry.payload.Value() : string("{}"); - auto ruleName = string("remediate") + mofEntry.ruleName; - auto result = engine.MmiSet(ruleName.c_str(), remediatePayload); - if (!result.HasValue()) - { - OsConfigLogError(logHandle.get(), "Failed to remediate: %s", result.Error().message.c_str()); - if (!options.continueOnError) - { - return 1; - } - hasError = true; - continue; - } - - auto error = benchmarkFormatter.AddEntry(mofEntry, result.Value(), "[]", engine.GetParameters(mofEntry.ruleName)); - if (error) - { - OsConfigLogError(logHandle.get(), "Failed to add entry to JSON formatter: %s", error.Value().message.c_str()); - if (!options.continueOnError) - { - return 1; - } - hasError = true; - continue; - } - - // Same allOf aggregation as the audit path. - status = CombineAllOf(status, result.Value()); - - break; - } - - default: - break; - } - } - - auto result = std::move(benchmarkFormatter).Finish(status); - if (!result.HasValue()) - { - OsConfigLogError(logHandle.get(), "Failed to finish formatted output: %s", result.Error().message.c_str()); - return 1; - } - - std::cout << result.Value() << "\n"; - return hasError ? 1 : 0; -} diff --git a/src/modules/complianceengine/src/assessor/Mof.cpp b/src/modules/complianceengine/src/assessor/Mof.cpp deleted file mode 100644 index cff4fb3bdd..0000000000 --- a/src/modules/complianceengine/src/assessor/Mof.cpp +++ /dev/null @@ -1,568 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#include "Mof.hpp" - -#include "InputSecurity.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ComplianceEngine -{ -namespace MOF -{ -using std::map; -using std::string; - -namespace -{ -// Upper bound on a single MOF line. MOF values (notably the base64-encoded -// ProcedureObjectValue) can be long, but a multi-megabyte line indicates a -// malformed or hostile input rather than a real benchmark entry. The line read -// stops as soon as this many bytes have accumulated, so a newline-free input -// cannot exhaust memory while we run as root. -constexpr size_t kMaxLineLength = static_cast(4) * 1024 * 1024; - -// Upper bound on the total number of input bytes consumed across all entries. -constexpr size_t kMaxInputBytes = static_cast(8) * 1024 * 1024; - -// Upper bound on the number of MOF entries processed from a single input. -constexpr size_t kMaxMofEntries = 100000; - -// Header that introduces each resource block, e.g. -// "instance of OsConfigResource as $OsConfigResource0ref". -constexpr char kHeaderPrefix[] = "instance of OsConfigResource as $OsConfigResource"; -constexpr char kHeaderSuffix[] = "ref"; - -// Header of the document metadata block the augmentation engine emits once per -// MOF (Version, Author, GenerationDate, Name). It carries no resource data and -// is skipped by the parser. -constexpr char kConfigurationDocumentHeader[] = "instance of OMI_ConfigurationDocument"; - -// The complete, fixed set of field keys the Compliance Augmentation Engine -// emits for every resource. The parser is strict: it rejects any key not in -// this set (unknown/extra fields) and requires every key in this set to be -// present (missing fields). This is the single source of truth for both checks. -const std::set& KnownMofKeys() -{ - static const std::set keys = {"ResourceID", "PayloadKey", "RuleId", "ComponentName", "ProcedureObjectName", "ProcedureObjectValue", - "InitObjectName", "ReportedObjectName", "ExpectedObjectValue", "DesiredObjectName", "DesiredObjectValue", "ModuleName", "ModuleVersion", - "ConfigurationName", "SourceInfo"}; - return keys; -} - -// Returns the suffix of `value` after `prefix`, or an empty Optional when -// `value` does not start with `prefix`. -Optional StripPrefix(const string& value, const string& prefix) -{ - if (value.size() < prefix.size() || value.compare(0, prefix.size(), prefix) != 0) - { - return Optional(); - } - return value.substr(prefix.size()); -} - -// Extracts the double-quoted value beginning at or after `from`, honouring \" -// and \\ escape sequences so that MOF string values like "{\"key\":\"val\"}" -// are returned unescaped as {"key":"val"}. Requires both an opening and a -// closing quote; a missing quote is a parse error. -Result ParseQuoted(const string& line, size_t from) -{ - const auto open = line.find('"', from); - if (open == string::npos) - { - return Error("MOF field value is not quoted: '" + line + "'", EINVAL); - } - - string result; - size_t pos = open + 1; - bool closed = false; - while (pos < line.size()) - { - const char c = line[pos]; - if (c == '\\' && pos + 1 < line.size()) - { - const char next = line[pos + 1]; - if (next == '"' || next == '\\') - { - result += next; - pos += 2; - continue; - } - } - if (c == '"') - { - closed = true; - break; - } - result += c; - ++pos; - } - - if (!closed) - { - return Error("MOF field value is missing a closing quote: '" + line + "'", EINVAL); - } - - const string tail = TrimWhiteSpaces(line.substr(pos + 1)); - if (tail != ";") - { - return Error("MOF field line must end with ';': '" + line + "'", EINVAL); - } - return result; -} - -// Parses a single `Key = "value";` field line into its key and unescaped value. -Result> ParseFieldLine(const string& line) -{ - const auto eq = line.find('='); - if (eq == string::npos) - { - return Error("MOF field line is missing '=': '" + line + "'", EINVAL); - } - - const string key = TrimWhiteSpaces(line.substr(0, eq)); - if (key.empty()) - { - return Error("MOF field line has an empty key: '" + line + "'", EINVAL); - } - - auto value = ParseQuoted(line, eq + 1); - if (!value.HasValue()) - { - return value.Error(); - } - return std::make_pair(key, std::move(value.Value())); -} - -// Validates a fully-collected field map and assembles the Resource. Enforces -// the constant fields, the required-field set, and rule-name consistency across -// the four object-name fields. The base64 ProcedureObjectValue is stored as-is; -// decoding and JSON validation are the ComplianceEngine's responsibility, which -// keeps the parser/engine boundary explicit. -Result BuildResource(const map& fields) -{ - for (const auto& key : KnownMofKeys()) - { - if (fields.find(key) == fields.end()) - { - return Error("MOF entry is missing required field: '" + key + "'", EINVAL); - } - } - - if (fields.at("ComponentName") != "ComplianceEngine") - { - return Error("MOF entry has unexpected ComponentName: '" + fields.at("ComponentName") + "'", EINVAL); - } - if (fields.at("ConfigurationName") != "ComplianceEngine") - { - return Error("MOF entry has unexpected ConfigurationName: '" + fields.at("ConfigurationName") + "'", EINVAL); - } - if (fields.at("ExpectedObjectValue") != "PASS") - { - return Error("MOF entry has unexpected ExpectedObjectValue: '" + fields.at("ExpectedObjectValue") + "'", EINVAL); - } - - const auto procedureName = StripPrefix(fields.at("ProcedureObjectName"), "procedure"); - if (!procedureName.HasValue()) - { - return Error("ProcedureObjectName must start with 'procedure': '" + fields.at("ProcedureObjectName") + "'", EINVAL); - } - const auto initName = StripPrefix(fields.at("InitObjectName"), "init"); - if (!initName.HasValue()) - { - return Error("InitObjectName must start with 'init': '" + fields.at("InitObjectName") + "'", EINVAL); - } - const auto auditName = StripPrefix(fields.at("ReportedObjectName"), "audit"); - if (!auditName.HasValue()) - { - return Error("ReportedObjectName must start with 'audit': '" + fields.at("ReportedObjectName") + "'", EINVAL); - } - const auto remediateName = StripPrefix(fields.at("DesiredObjectName"), "remediate"); - if (!remediateName.HasValue()) - { - return Error("DesiredObjectName must start with 'remediate': '" + fields.at("DesiredObjectName") + "'", EINVAL); - } - - const string& ruleName = procedureName.Value(); - if (ruleName.empty()) - { - return Error("MOF entry has an empty rule name", EINVAL); - } - if (initName.Value() != ruleName || auditName.Value() != ruleName || remediateName.Value() != ruleName) - { - return Error("MOF entry object names disagree on the rule name '" + ruleName + "'", EINVAL); - } - - if (fields.at("ResourceID").empty()) - { - return Error("MOF entry has an empty ResourceID", EINVAL); - } - if (fields.at("ProcedureObjectValue").empty()) - { - return Error("MOF entry has an empty ProcedureObjectValue", EINVAL); - } - - auto benchmarkInfo = CISBenchmarkInfo::Parse(fields.at("PayloadKey")); - if (!benchmarkInfo.HasValue()) - { - return Error("Failed to parse PayloadKey: " + benchmarkInfo.Error().message, benchmarkInfo.Error().code); - } - - Resource resource; - resource.resourceID = fields.at("ResourceID"); - resource.ruleId = fields.at("RuleId"); - resource.benchmarkInfo = std::move(benchmarkInfo.Value()); - // The section in the payload key is '/'-separated (e.g. "1/1/1/1"); the rest - // of the assessor expects dotted notation (e.g. "1.1.1.1"). - std::replace(resource.benchmarkInfo.section.begin(), resource.benchmarkInfo.section.end(), '/', '.'); - resource.procedure = fields.at("ProcedureObjectValue"); - resource.ruleName = ruleName; - resource.hasInitAudit = true; // InitObjectName is required and validated above. - - // An empty DesiredObjectValue (emitted for every rule today) is modelled as - // an absent payload; a non-empty value is carried through. - const string& desired = fields.at("DesiredObjectValue"); - if (!desired.empty()) - { - resource.payload = desired; - } - - return resource; -} -} // anonymous namespace - -MofResourceRange::MofResourceRange(std::istream& stream, OsConfigLogHandle logHandle) noexcept - : mStream(&stream), - mLog(logHandle), - mOwnedBuf(), - mOwnedStream() - -{ -} - -MofResourceRange::MofResourceRange(MofResourceRange&& other) noexcept - : mStream(other.mStream), - mLog(other.mLog), - mOwnedBuf(std::move(other.mOwnedBuf)), - mOwnedStream(std::move(other.mOwnedStream)), - mBytesConsumed(other.mBytesConsumed), - mEntryCount(other.mEntryCount) -{ - other.mStream = nullptr; -} - -Result MofResourceRange::Make(const string& path, OsConfigLogHandle logHandle) -{ - // Encapsulate the full input-hardening posture so callers never open the - // file themselves: reject path traversal, require a root-owned non-writable - // parent directory, and open with O_NOFOLLOW plus regular-file/ownership/ - // mode checks on the resulting fd. - if (Assessor::RefusePathTraversal(path, logHandle)) - { - return Error("Refusing to open MOF input with an unsafe path: '" + path + "'", EACCES); - } - if (Assessor::RefuseWritableParentDir(path, logHandle)) - { - return Error("Refusing to open MOF input in a writable parent directory: '" + path + "'", EACCES); - } - auto fdResult = Assessor::OpenVerifiedInput(path, logHandle); - if (!fdResult.HasValue()) - { - return fdResult.Error(); - } - - // Bridge the verified fd into a std::istream for streaming. stdio_filebuf - // takes ownership of the fd and closes it when destroyed. - std::unique_ptr<__gnu_cxx::stdio_filebuf> buffer(new __gnu_cxx::stdio_filebuf(fdResult.Value(), std::ios_base::in)); - std::unique_ptr stream(new std::istream(buffer.get())); - - MofResourceRange range(*stream, logHandle); - range.mOwnedBuf = std::move(buffer); - range.mOwnedStream = std::move(stream); - range.mStream = range.mOwnedStream.get(); - return range; -} - -Result MofResourceRange::MakeFromStream(std::istream& stream, OsConfigLogHandle logHandle) -{ - return MofResourceRange(stream, logHandle); -} - -Result MofResourceRange::Make(std::istream& stream, OsConfigLogHandle logHandle) -{ - return MofResourceRange(stream, logHandle); -} - -MofResourceIterator MofResourceRange::begin() // NOLINT(*-identifier-naming) -{ - return MofResourceIterator(*this); -} - -MofResourceIterator MofResourceRange::end() // NOLINT(*-identifier-naming) -{ - return MofResourceIterator(); -} - -Result> MofResourceRange::ParseNext() -{ - // Reads a single line from the stream. Returns: - // Error — a resource cap was exceeded (line too long, total bytes too large) - // Optional() — clean end of input (EOF with nothing buffered) - // Optional(line) — one complete line (newline consumed but not included) - // - // A non-empty partial line at EOF (no trailing newline) is returned as an - // error: the augmentation engine always terminates every block with '};' - // followed by a newline, so a line without a terminator means the input was - // truncated. - const auto readLine = [this]() -> Result> { - string line; - std::istream& stream = *mStream; - while (true) - { - const std::istream::int_type ch = stream.get(); - if (ch == std::istream::traits_type::eof()) - { - if (stream.bad()) - { - return Error("I/O error reading MOF input", EIO); - } - if (line.empty()) - { - return Optional(); // Clean EOF. - } - return Error("Truncated MOF input: last line has no newline terminator", EIO); - } - if (ch == '\n') - { - // Strip a trailing '\r' to handle CRLF line endings. - if (!line.empty() && line.back() == '\r') - { - line.pop_back(); - } - // Count the line terminator alongside the content. - if (++mBytesConsumed > kMaxInputBytes) - { - return Error("MOF input exceeds the maximum size of " + std::to_string(kMaxInputBytes) + " bytes", E2BIG); - } - return Optional(std::move(line)); - } - if (line.size() >= kMaxLineLength) - { - return Error("MOF line exceeds the maximum length of " + std::to_string(kMaxLineLength) + " bytes", E2BIG); - } - line.push_back(static_cast(ch)); - if (++mBytesConsumed > kMaxInputBytes) - { - return Error("MOF input exceeds the maximum size of " + std::to_string(kMaxInputBytes) + " bytes", E2BIG); - } - } - }; - - // Locate the next entry header, skipping blank lines between entries. A - // clean end of input here means there are no more entries. The document - // metadata block (OMI_ConfigurationDocument) carries no resource data and - // is skipped in its entirety. - string header; - while (true) - { - auto read = readLine(); - if (!read.HasValue()) - { - return read.Error(); - } - if (!read.Value().HasValue()) - { - return Optional(); // Clean EOF — no more entries. - } - header = TrimWhiteSpaces(read.Value().Value()); - if (header.empty()) - { - continue; - } - if (header == kConfigurationDocumentHeader) - { - // Consume the block ('{' ... '};') and resume the header search. - // The opening brace is required on its own line, exactly as for a - // normal resource entry; without this check a malformed block - // missing its '{' would swallow lines up to the next entry's '};' - // and silently drop that entry. - auto openRead = readLine(); - if (!openRead.HasValue()) - { - return openRead.Error(); - } - if (!openRead.Value().HasValue() || TrimWhiteSpaces(openRead.Value().Value()) != "{") - { - return Error("Expected '{' after OMI_ConfigurationDocument header", EINVAL); - } - while (true) - { - auto blockRead = readLine(); - if (!blockRead.HasValue()) - { - return blockRead.Error(); - } - if (!blockRead.Value().HasValue()) - { - return Error("Truncated MOF entry: missing closing '};'", EIO); - } - if (TrimWhiteSpaces(blockRead.Value().Value()) == "};") - { - break; - } - } - continue; - } - break; - } - const size_t prefixLen = sizeof(kHeaderPrefix) - 1; - const size_t suffixLen = sizeof(kHeaderSuffix) - 1; - if (header.size() < prefixLen + suffixLen || header.compare(0, prefixLen, kHeaderPrefix) != 0 || - header.compare(header.size() - suffixLen, suffixLen, kHeaderSuffix) != 0) - { - return Error("Malformed MOF entry header: '" + header + "'", EINVAL); - } - - if (++mEntryCount > kMaxMofEntries) - { - return Error("MOF input exceeds the maximum of " + std::to_string(kMaxMofEntries) + " entries", E2BIG); - } - - // The header must be followed by an opening brace on its own line. - { - auto read = readLine(); - if (!read.HasValue()) - { - return read.Error(); - } - if (!read.Value().HasValue() || TrimWhiteSpaces(read.Value().Value()) != "{") - { - return Error("Expected '{' after MOF entry header", EINVAL); - } - } - - // Collect the field lines up to the closing '};', rejecting unknown and - // duplicate keys. - map fields; - bool closed = false; - while (true) - { - auto read = readLine(); - if (!read.HasValue()) - { - return read.Error(); - } - if (!read.Value().HasValue()) - { - break; // EOF before '};' - } - - const string trimmed = TrimWhiteSpaces(read.Value().Value()); - if (trimmed.empty()) - { - continue; - } - if (trimmed == "};") - { - closed = true; - break; - } - - auto field = ParseFieldLine(trimmed); - if (!field.HasValue()) - { - return field.Error(); - } - if (KnownMofKeys().find(field.Value().first) == KnownMofKeys().end()) - { - return Error("Unknown MOF field key: '" + field.Value().first + "'", EINVAL); - } - if (!fields.emplace(field.Value().first, std::move(field.Value().second)).second) - { - return Error("Duplicate MOF field key: '" + field.Value().first + "'", EINVAL); - } - } - - if (!closed) - { - return Error("Truncated MOF entry: missing closing '};'", EIO); - } - - auto resource = BuildResource(fields); - if (!resource.HasValue()) - { - return resource.Error(); - } - return Optional(std::move(resource.Value())); -} - -MofResourceIterator::MofResourceIterator(MofResourceRange& range) - : mRange(&range) -{ - Advance(); -} - -void MofResourceIterator::Advance() -{ - auto result = mRange->ParseNext(); - if (!result.HasValue()) - { - // Surface the parse error once; the next increment terminates iteration - // because a desynchronized stream cannot be resumed reliably. - mCurrent = Result(result.Error()); - mErrored = true; - return; - } - - Optional& entry = result.Value(); - if (!entry.HasValue()) - { - mRange = nullptr; // Clean end of input. - return; - } - mCurrent = Result(std::move(entry.Value())); -} - -MofResourceIterator& MofResourceIterator::operator++() -{ - if (mErrored) - { - mRange = nullptr; - return *this; - } - if (mRange != nullptr) - { - Advance(); - } - return *this; -} - -MofResourceIterator::reference MofResourceIterator::operator*() const -{ - return mCurrent; -} - -MofResourceIterator::pointer MofResourceIterator::operator->() const -{ - return &mCurrent; -} - -bool MofResourceIterator::operator==(const MofResourceIterator& other) const -{ - return mRange == other.mRange; -} - -bool MofResourceIterator::operator!=(const MofResourceIterator& other) const -{ - return mRange != other.mRange; -} -} // namespace MOF -} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/assessor/Mof.hpp b/src/modules/complianceengine/src/assessor/Mof.hpp deleted file mode 100644 index 9ba39e8fb9..0000000000 --- a/src/modules/complianceengine/src/assessor/Mof.hpp +++ /dev/null @@ -1,162 +0,0 @@ -#ifndef COMPLIANCE_ENGINE_ASSESSOR_MOF_HPP -#define COMPLIANCE_ENGINE_ASSESSOR_MOF_HPP - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ComplianceEngine -{ -namespace MOF -{ -// A single parsed MOF resource entry. -// -// Only the fields actually consumed by the assessor's main loop and output -// formatters are stored. The parser validates every field the augmentation -// engine emits (see MofResourceRange), but deliberately discards the ones the -// assessor does not use (the constant ComponentName/ExpectedObjectValue/ -// ConfigurationName/ModuleName fields, ModuleVersion, SourceInfo, etc.). -struct Resource -{ - // ResourceID, e.g. "1.1.1.1 Ensure cramfs kernel module is not available". - // Emitted in the canonical result JSON as `title` (the human-readable rule - // title), reusing the definition's field name. - std::string resourceID; - - // RuleId: the stable, benchmark-agnostic rule identifier (a UUID derived - // from the payload key by the augmentation engine). Retained and emitted in - // the canonical result JSON as `ruleId` so tooling can join to a rule - // reliably rather than matching on ruleName/section. - std::string ruleId; - - // Benchmark info parsed from the PayloadKey. `.distribution` and `.version` - // drive the applicability check in the main loop (Match against the detected - // system), `.section` drives section filtering (main loop and JSON - // formatter); the whole struct is retained so the formatters need no changes. - CISBenchmarkInfo benchmarkInfo; - - // Base64-encoded rule payload (ProcedureObjectValue). The parser does NOT - // decode or validate this blob; base64/JSON parsing is owned by the - // ComplianceEngine (Engine/Procedure), which keeps the parser/engine - // boundary clear. See MofResourceRange for the validation the parser does do. - std::string procedure; - - // DesiredObjectValue. The augmentation engine emits an empty string for - // every rule today; an empty value is modelled as an absent payload. - Optional payload; - - // The rule name shared by the ProcedureObjectName ("procedure"), - // InitObjectName ("init"), ReportedObjectName ("audit") and - // DesiredObjectName ("remediate") fields. The parser enforces that all - // four agree before storing the common suffix here. - std::string ruleName; - - // True when the entry carries an InitObjectName (the engine always emits one). - bool hasInitAudit = false; -}; - -class MofResourceRange; - -// Input iterator over the MOF resource entries in a stream. -// -// Dereferencing yields a `const Result&`: a per-entry parse error is -// delivered in-band as an Error value so the caller can check each entry and -// bail out, rather than being thrown. Once an entry fails to parse (or end of -// input is reached) the iterator becomes equal to end(); a desynchronized -// stream cannot be resumed reliably, so iteration stops. -class MofResourceIterator -{ -public: - using iterator_category = std::input_iterator_tag; - using value_type = Result; - using difference_type = std::ptrdiff_t; - using pointer = const Result*; - using reference = const Result&; - - reference operator*() const; - pointer operator->() const; - MofResourceIterator& operator++(); - bool operator==(const MofResourceIterator& other) const; - bool operator!=(const MofResourceIterator& other) const; - -private: - friend class MofResourceRange; - explicit MofResourceIterator(MofResourceRange& range); - MofResourceIterator() = default; // end iterator - - void Advance(); - - MofResourceRange* mRange = nullptr; // nullptr == end - bool mErrored = false; - Result mCurrent = ComplianceEngine::Error("uninitialized MOF iterator"); -}; - -// Owns the input stream (RAII) and streams strictly-validated MOF resource -// entries from it. Construct via the Make* factories, which encapsulate the -// input-hardening safeguards so callers never open the input file themselves. -// -// Strictness: the parser targets the exact, regular format emitted by the -// Compliance Augmentation Engine. It requires every expected field to be -// present, rejects unknown field keys, validates the constant fields -// (ComponentName/ExpectedObjectValue/ConfigurationName) and enforces that the -// four object-name fields share a single rule name. The base64 ProcedureObject -// payload is passed through untouched; decoding/JSON validation is the -// ComplianceEngine's responsibility. -class MofResourceRange -{ -public: - // Opens a regular file on disk, applying the full input-hardening posture - // (path-traversal rejection, root-owned non-writable parent directory, - // O_NOFOLLOW open, regular-file/ownership/mode fstat checks) before the - // first byte is read. The verified fd is owned and closed on destruction. - static Result Make(const std::string& path, OsConfigLogHandle logHandle); - - // Streams from stdin (or any externally-owned istream representing stdin). - // The stream is not owned. stdin has no on-disk identity, so the file-based - // safeguards do not apply, but the size/line/entry caps still bound resource - // usage. - static Result Make(std::istream& stream, OsConfigLogHandle logHandle); - - // Streams from an externally-owned istream. Intended for unit tests and the - // fuzzer; applies the same strict parsing and caps as the other factories. - static Result MakeFromStream(std::istream& stream, OsConfigLogHandle logHandle); - - MofResourceRange(MofResourceRange&& other) noexcept; - MofResourceRange& operator=(MofResourceRange&&) = delete; - MofResourceRange(const MofResourceRange&) = delete; - MofResourceRange& operator=(const MofResourceRange&) = delete; - ~MofResourceRange() = default; - - MofResourceIterator begin(); // NOLINT(*-identifier-naming) - MofResourceIterator end(); // NOLINT(*-identifier-naming) - -private: - friend class MofResourceIterator; - MofResourceRange(std::istream& stream, OsConfigLogHandle logHandle) noexcept; - - // Parses the next entry from the stream, enforcing the size/line/entry caps. - // Returns: - // - a Resource -> a parsed entry, - // - an empty Optional -> clean end of input (no more entries), - // - an Error -> malformed input or a cap was exceeded. - Result> ParseNext(); - - std::istream* mStream; - OsConfigLogHandle mLog; - // For file inputs the range owns the streambuf that bridges the verified fd - // into an istream (and the istream itself). For stdin/test inputs these are - // null and mStream references the caller's stream. Declared before - // mOwnedStream so the istream is destroyed before the streambuf it uses. - std::unique_ptr mOwnedBuf; - std::unique_ptr mOwnedStream; - size_t mBytesConsumed = 0; - size_t mEntryCount = 0; -}; -} // namespace MOF -} // namespace ComplianceEngine -#endif // COMPLIANCE_ENGINE_ASSESSOR_MOF_HPP diff --git a/src/modules/complianceengine/src/assessor/TextRenderers.cpp b/src/modules/complianceengine/src/assessor/TextRenderers.cpp deleted file mode 100644 index 6c8e03fe6d..0000000000 --- a/src/modules/complianceengine/src/assessor/TextRenderers.cpp +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#include -#include -#include -#include -#include -#include - -namespace ComplianceEngine -{ -namespace Assessor -{ -using std::string; - -namespace -{ -// Recursively appends an indicator tree as indented "