From 2cb8e5d973e79bd84b3f7e093f8577213785e838 Mon Sep 17 00:00:00 2001 From: Charity Kathure Date: Tue, 15 Apr 2025 17:50:03 +0300 Subject: [PATCH 1/4] Refactor JSON Parsing to Use Boost.JSON (#202) * boost json parser Signed-off-by: Charity Kathure Co-authored-by: Charity Kathure --- .github/workflows/sdl-compliance-pipeline.yml | 2 +- LogMonitor/CMakeLists.txt | 36 + LogMonitor/LogMonitorTests/CMakeLists.txt | 42 + .../LogMonitorTests/ConfigFileParserTests.cpp | 1759 ----------------- .../LogMonitorTests/JsonProcessorTests.cpp | 23 + .../LogMonitorTests/LogMonitorTests.cpp | 1 - .../LogMonitorTests/LogMonitorTests.vcxproj | 2 +- .../LogMonitorTests.vcxproj.filters | 6 +- LogMonitor/src/CMakeLists.txt | 23 + .../src/LogMonitor/ConfigFileParser.cpp | 774 -------- LogMonitor/src/LogMonitor/EtwMonitor.cpp | 2 +- LogMonitor/src/LogMonitor/JsonProcessor.cpp | 457 +++++ LogMonitor/src/LogMonitor/JsonProcessor.h | 58 + LogMonitor/src/LogMonitor/LogMonitor.vcxproj | 6 +- .../src/LogMonitor/LogMonitor.vcxproj.filters | 16 +- LogMonitor/src/LogMonitor/Main.cpp | 2 +- .../src/LogMonitor/Parser/LoggerSettings.h | 6 + LogMonitor/src/LogMonitor/Utility.cpp | 19 + LogMonitor/src/LogMonitor/Utility.h | 4 + LogMonitor/src/LogMonitor/pch.h | 2 + azure-pipelines.yml | 163 +- 21 files changed, 798 insertions(+), 2605 deletions(-) create mode 100644 LogMonitor/CMakeLists.txt create mode 100644 LogMonitor/LogMonitorTests/CMakeLists.txt delete mode 100644 LogMonitor/LogMonitorTests/ConfigFileParserTests.cpp create mode 100644 LogMonitor/LogMonitorTests/JsonProcessorTests.cpp create mode 100644 LogMonitor/src/CMakeLists.txt delete mode 100644 LogMonitor/src/LogMonitor/ConfigFileParser.cpp create mode 100644 LogMonitor/src/LogMonitor/JsonProcessor.cpp create mode 100644 LogMonitor/src/LogMonitor/JsonProcessor.h diff --git a/.github/workflows/sdl-compliance-pipeline.yml b/.github/workflows/sdl-compliance-pipeline.yml index 6291facd..16e18a7a 100644 --- a/.github/workflows/sdl-compliance-pipeline.yml +++ b/.github/workflows/sdl-compliance-pipeline.yml @@ -84,4 +84,4 @@ jobs: with: # The path of the directory in which to save the SARIF results (../results/cpp.sarif) output: ${{ env.CodeQLResultsDir }} - upload: "always" # Options: 'always', 'failure-only', 'never' + upload: "always" # Options: 'always', 'failure-only', 'never' \ No newline at end of file diff --git a/LogMonitor/CMakeLists.txt b/LogMonitor/CMakeLists.txt new file mode 100644 index 00000000..dfbbd4b8 --- /dev/null +++ b/LogMonitor/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.15) + +# Define the project +project(LogMonitor) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_BUILD_TYPE Release) +set(VCPKG_TARGET_TRIPLET x64-windows) + +# Use vcpkg if available +if (DEFINED ENV{VCPKG_ROOT}) + set(VCPKG_ROOT $ENV{VCPKG_ROOT}) + set(CMAKE_TOOLCHAIN_FILE "${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Toolchain file for vcpkg" FORCE) + + # Set Boost paths + set(BOOST_ROOT "${VCPKG_ROOT}/installed/x64-windows" CACHE PATH "Boost installation root") + set(Boost_INCLUDE_DIR "${VCPKG_ROOT}/installed/x64-windows/include" CACHE PATH "Boost include directory") +endif() + +# Set Windows SDK version if available +if (DEFINED ENV{SDKVersion}) + set(CMAKE_SYSTEM_VERSION $ENV{SDKVersion}) +endif() + +# Enable testing framework +enable_testing() + +# Enable Unicode globally +add_definitions(-DUNICODE -D_UNICODE) + +# Include subdirectories for main and test executables +add_subdirectory(src) # Add main executable's CMake +add_subdirectory(LogMonitorTests) # Add test executable's CMake diff --git a/LogMonitor/LogMonitorTests/CMakeLists.txt b/LogMonitor/LogMonitorTests/CMakeLists.txt new file mode 100644 index 00000000..79c50abd --- /dev/null +++ b/LogMonitor/LogMonitorTests/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.15) + +project(LogMonitorTests) + +find_package(Boost REQUIRED COMPONENTS json) + +# Automatically gather all test source files +file(GLOB_RECURSE TEST_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*.cpp") + +# Ensure all source files exist before proceeding +if(NOT TEST_SOURCES) + message(FATAL_ERROR "No valid source files found for LogMonitorTests.") +endif() + +# Define test shared library (DLL) +add_library(LogMonitorTests SHARED ${TEST_SOURCES}) + +# Add a definition for symbol exporting +target_compile_definitions(LogMonitorTests PRIVATE LOGMONITORTESTS_EXPORTS) + +# Include directories (for headers) +target_include_directories(LogMonitorTests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} # Includes Utility.h and pch.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ${Boost_INCLUDE_DIRS} +) + +# Set Windows-specific linker flags +set_target_properties(LogMonitorTests PROPERTIES + COMPILE_PDB_NAME "LogMonitorTests" + COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + LINK_FLAGS "/DEBUG:FULL /OPT:REF /OPT:ICF" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" +) + +# Link LogMonitor and Boost.JSON +target_link_libraries(LogMonitorTests PRIVATE LogMonitor Boost::json) + +# Enable testing +enable_testing() + +add_test(NAME LogMonitorTests COMMAND LogMonitorTests) diff --git a/LogMonitor/LogMonitorTests/ConfigFileParserTests.cpp b/LogMonitor/LogMonitorTests/ConfigFileParserTests.cpp deleted file mode 100644 index f0c0b054..00000000 --- a/LogMonitor/LogMonitorTests/ConfigFileParserTests.cpp +++ /dev/null @@ -1,1759 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// - -#include "pch.h" - -using namespace Microsoft::VisualStudio::CppUnitTestFramework; - -#define BUFFER_SIZE 65536 - -namespace LogMonitorTests -{ - /// - /// Tests of the ConfigFileParser's ReadConfigFile function. This function - /// uses a JsonFileParser object to parse a JSON string, and return a - /// vector of sources with the retrieved configuration. - /// - TEST_CLASS(ConfigFileParserTests) - { - WCHAR bigOutBuf[BUFFER_SIZE]; - - /// - /// Gets the content of the Stdout buffer and returns it in a wstring. - /// - /// \return A wstring with the stdout. - /// - std::wstring RecoverOuput() - { - return std::wstring(bigOutBuf); - } - - /// - /// Removes the braces at the start and end of a string. - /// - /// \param Str A wstring. - /// - /// \return A wstring. - /// - std::wstring RemoveBracesGuidStr(const std::wstring& str) - { - if (str.size() >= 2 && str[0] == L'{' && str[str.length() - 1] == L'}') - { - return str.substr(1, str.length() - 2); - } - return str; - } - - /// - /// Add the path of the created directories, to be removed during - /// cleanup. - /// - std::vector directoriesToDeleteAtCleanup; - - public: - - /// - /// "Redirects" the stdout to our buffer. - /// - TEST_METHOD_INITIALIZE(InitializeLogFileMonitorTests) - { - // - // Set our own buffer in stdout. - // - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - fflush(stdout); - _setmode(_fileno(stdout), _O_U16TEXT); - setvbuf(stdout, (char*)bigOutBuf, _IOFBF, sizeof(bigOutBuf) - sizeof(WCHAR)); - } - - /// - /// Test that the most simple, but valid configuration string is - /// read successfully. - /// - TEST_METHOD(TestBasicConfigFile) - { - std::wstring configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - ]\ - }\ - }"; - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - } - - /// - /// Tests that EventLog sources, with all their attributes, are read - /// successfully. - /// - TEST_METHOD(TestSourceEventLog) - { - bool startAtOldestRecord = true; - bool eventFormatMultiLine = true; - - // - // Template of a valid configuration string, with an EventLog source. - // - std::wstring configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"EventLog\",\ - \"startAtOldestRecord\" : %s,\ - \"eventFormatMultiLine\" : %s,\ - \"channels\" : [\ - {\ - \"name\": \"%s\",\ - \"level\" : \"%s\"\ - },\ - {\ - \"name\": \"%s\",\ - \"level\" : \"%s\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - // - // Try reading this values - // - std::wstring firstChannelName = L"system"; - EventChannelLogLevel firstChannelLevel = EventChannelLogLevel::Information; - - std::wstring secondChannelName = L"application"; - EventChannelLogLevel secondChannelLevel = EventChannelLogLevel::Critical; - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - startAtOldestRecord ? L"true" : L"false", - eventFormatMultiLine ? L"true" : L"false", - firstChannelName.c_str(), - LogLevelNames[(int)firstChannelLevel - 1].c_str(), - secondChannelName.c_str(), - LogLevelNames[(int)secondChannelLevel - 1].c_str() - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::EventLog, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEventLog = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(startAtOldestRecord, sourceEventLog->StartAtOldestRecord); - Assert::AreEqual(eventFormatMultiLine, sourceEventLog->EventFormatMultiLine); - - Assert::AreEqual((size_t)2, sourceEventLog->Channels.size()); - - Assert::AreEqual(firstChannelName.c_str(), sourceEventLog->Channels[0].Name.c_str()); - Assert::AreEqual((int)firstChannelLevel, (int)sourceEventLog->Channels[0].Level); - - Assert::AreEqual(secondChannelName.c_str(), sourceEventLog->Channels[1].Name.c_str()); - Assert::AreEqual((int)secondChannelLevel, (int)sourceEventLog->Channels[1].Level); - } - - // - // Try with different values - // - startAtOldestRecord = false; - eventFormatMultiLine = false; - - firstChannelName = L"security"; - firstChannelLevel = EventChannelLogLevel::Error; - - secondChannelName = L"kernel"; - secondChannelLevel = EventChannelLogLevel::Warning; - - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - startAtOldestRecord ? L"true" : L"false", - eventFormatMultiLine ? L"true" : L"false", - firstChannelName.c_str(), - LogLevelNames[(int)firstChannelLevel - 1].c_str(), - secondChannelName.c_str(), - LogLevelNames[(int)secondChannelLevel - 1].c_str() - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::EventLog, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEventLog = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(startAtOldestRecord, sourceEventLog->StartAtOldestRecord); - Assert::AreEqual(eventFormatMultiLine, sourceEventLog->EventFormatMultiLine); - - Assert::AreEqual((size_t)2, sourceEventLog->Channels.size()); - - Assert::AreEqual(firstChannelName.c_str(), sourceEventLog->Channels[0].Name.c_str()); - Assert::AreEqual((int)firstChannelLevel, (int)sourceEventLog->Channels[0].Level); - - Assert::AreEqual(secondChannelName.c_str(), sourceEventLog->Channels[1].Name.c_str()); - Assert::AreEqual((int)secondChannelLevel, (int)sourceEventLog->Channels[1].Level); - } - } - - /// - /// Test that default values for optional attributes on an EventLog source - /// are correct. - /// - TEST_METHOD(TestSourceEventLogDefaultValues) - { - std::wstring configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"EventLog\",\ - \"channels\" : [\ - {\ - \"name\": \"system\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::EventLog, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEventLog = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(false, sourceEventLog->StartAtOldestRecord); - Assert::AreEqual(true, sourceEventLog->EventFormatMultiLine); - - Assert::AreEqual((size_t)1, sourceEventLog->Channels.size()); - - Assert::AreEqual((int)EventChannelLogLevel::Error, (int)sourceEventLog->Channels[0].Level); - } - } - - /// - /// Tests that file sources, with all their attributes, are read - /// successfully. - /// - TEST_METHOD(TestSourceFile) - { - // - // Template of a valid configuration string, with a file source. - // - std::wstring configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"File\",\ - \"directory\": \"%s\",\ - \"filter\": \"%s\",\ - \"includeSubdirectories\": %s\ - }\ - ]\ - }\ - }"; - - // - // First, try with this values. - // - bool includeSubdirectories = true; - - std::wstring directory = L"C:\\LogMonitor\\logs"; - std::wstring filter = L"*.*"; - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - Utility::ReplaceAll(directory, L"\\", L"\\\\").c_str(), - filter.c_str(), - includeSubdirectories ? L"true" : L"false" - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::File, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceFile = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(directory.c_str(), sourceFile->Directory.c_str()); - Assert::AreEqual(filter.c_str(), sourceFile->Filter.c_str()); - Assert::AreEqual(includeSubdirectories, sourceFile->IncludeSubdirectories); - } - - // - // Try with different values - // - includeSubdirectories = false; - - directory = L"c:\\\\inetpub\\\\logs"; - filter = L"*.log"; - - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - Utility::ReplaceAll(directory, L"\\", L"\\\\").c_str(), - filter.c_str(), - includeSubdirectories ? L"true" : L"false" - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::File, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceFile = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(directory.c_str(), sourceFile->Directory.c_str()); - Assert::AreEqual(filter.c_str(), sourceFile->Filter.c_str()); - Assert::AreEqual(includeSubdirectories, sourceFile->IncludeSubdirectories); - } - } - - /// - /// Test that default values for optional attributes on a file source are correct. - /// - TEST_METHOD(TestSourceFileDefaultValues) - { - - std::wstring directory = L"C:\\LogMonitor\\logs"; - - std::wstring configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"File\",\ - \"directory\": \"%s\"\ - }\ - ]\ - }\ - }"; - - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - Utility::ReplaceAll(directory, L"\\", L"\\\\").c_str() - ); - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::File, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceFile = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(directory.c_str(), sourceFile->Directory.c_str()); - Assert::AreEqual(L"", sourceFile->Filter.c_str()); - Assert::AreEqual(false, sourceFile->IncludeSubdirectories); - Assert::AreEqual(300.0, sourceFile->WaitInSeconds); - } - } - - /// - /// Tests that etw sources, with all their attributes, are read - /// successfully. - /// - TEST_METHOD(TestSourceETW) - { - HRESULT hr; - - // - // Used to convert an etw log level value to its string representation. - // - const static std::vector c_LevelToString = - { - L"Unknown", - L"Critical", - L"Error", - L"Warning", - L"Information", - L"Verbose", - }; - - // - // Template of a valid configuration string, with a file source. - // - std::wstring configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"ETW\",\ - \"eventFormatMultiLine\" : %s,\ - \"providers\" : [\ - {\ - \"providerName\": \"%s\",\ - \"providerGuid\": \"%s\",\ - \"level\" : \"%s\",\ - \"keywords\" : \"%llu\"\ - },\ - {\ - \"providerName\": \"%s\",\ - \"providerGuid\": \"%s\",\ - \"level\" : \"%s\",\ - \"keywords\" : \"%llu\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - // - // First, try this values. - // - bool eventFormatMultiLine = true; - - std::wstring firstProviderName = L"IIS: WWW Server"; - std::wstring firstProviderGuid = L"3A2A4E84-4C21-4981-AE10-3FDA0D9B0F83"; - UCHAR firstProviderLevel = 2; // Error - ULONGLONG firstProviderKeywords = 255; - - std::wstring secondProviderName = L"Microsoft-Windows-IIS-Logging"; - std::wstring secondProviderGuid = L"{7E8AD27F-B271-4EA2-A783-A47BDE29143B}"; - UCHAR secondProviderLevel = 1; // Critical - ULONGLONG secondProviderKeywords = 555; - - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - eventFormatMultiLine ? L"true" : L"false", - firstProviderName.c_str(), - firstProviderGuid.c_str(), - c_LevelToString[(int)firstProviderLevel].c_str(), - firstProviderKeywords, - secondProviderName.c_str(), - secondProviderGuid.c_str(), - c_LevelToString[(int)secondProviderLevel].c_str(), - secondProviderKeywords - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::ETW, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEtw = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(eventFormatMultiLine, sourceEtw->EventFormatMultiLine); - - Assert::AreEqual((size_t)2, sourceEtw->Providers.size()); - - // - // First provider - // - Assert::AreEqual(firstProviderName.c_str(), sourceEtw->Providers[0].ProviderName.c_str()); - Assert::AreEqual(firstProviderLevel, sourceEtw->Providers[0].Level); - Assert::AreEqual(firstProviderKeywords, sourceEtw->Providers[0].Keywords); - - // - // Check that guids are equal - // - LPWSTR providerGuid1 = NULL; - hr = StringFromCLSID(sourceEtw->Providers[0].ProviderGuid, &providerGuid1); - Assert::IsFalse(FAILED(hr)); - Assert::AreEqual(RemoveBracesGuidStr(firstProviderGuid).c_str(), RemoveBracesGuidStr(std::wstring(providerGuid1)).c_str()); - CoTaskMemFree(providerGuid1); - - // - // Second provider - // - Assert::AreEqual(secondProviderName.c_str(), sourceEtw->Providers[1].ProviderName.c_str()); - Assert::AreEqual(secondProviderLevel, sourceEtw->Providers[1].Level); - Assert::AreEqual(secondProviderKeywords, sourceEtw->Providers[1].Keywords); - - // - // Check that guids are equal - // - LPWSTR providerGuid2 = NULL; - hr = StringFromCLSID(sourceEtw->Providers[1].ProviderGuid, &providerGuid2); - Assert::IsFalse(FAILED(hr)); - Assert::AreEqual(RemoveBracesGuidStr(secondProviderGuid).c_str(), RemoveBracesGuidStr(std::wstring(providerGuid2)).c_str()); - CoTaskMemFree(providerGuid2); - } - - // - // Try different values - // - eventFormatMultiLine = false; - - firstProviderName = L"Microsoft-Windows-SMBClient"; - firstProviderGuid = L"{988C59C5-0A1C-45B6-A555-0C62276E327D}"; - firstProviderLevel = 3; // Warning - firstProviderKeywords = 0xff; - - secondProviderName = L"Microsoft-Windows-SMBWitnessClient"; - secondProviderGuid = L"32254F6C-AA33-46F0-A5E3-1CBCC74BF683"; - secondProviderLevel = 4; // Information - secondProviderKeywords = 0xfe; - - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - eventFormatMultiLine ? L"true" : L"false", - firstProviderName.c_str(), - firstProviderGuid.c_str(), - c_LevelToString[(int)firstProviderLevel].c_str(), - firstProviderKeywords, - secondProviderName.c_str(), - secondProviderGuid.c_str(), - c_LevelToString[(int)secondProviderLevel].c_str(), - secondProviderKeywords - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::ETW, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEtw = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(eventFormatMultiLine, sourceEtw->EventFormatMultiLine); - - Assert::AreEqual((size_t)2, sourceEtw->Providers.size()); - - // - // First provider - // - Assert::AreEqual(firstProviderName.c_str(), sourceEtw->Providers[0].ProviderName.c_str()); - Assert::AreEqual(firstProviderLevel, sourceEtw->Providers[0].Level); - Assert::AreEqual(firstProviderKeywords, sourceEtw->Providers[0].Keywords); - - // - // Check that guids are equal - // - LPWSTR providerGuid1 = NULL; - hr = StringFromCLSID(sourceEtw->Providers[0].ProviderGuid, &providerGuid1); - Assert::IsFalse(FAILED(hr)); - Assert::AreEqual(RemoveBracesGuidStr(firstProviderGuid).c_str(), RemoveBracesGuidStr(std::wstring(providerGuid1)).c_str()); - CoTaskMemFree(providerGuid1); - - // - // Second provider - // - Assert::AreEqual(secondProviderName.c_str(), sourceEtw->Providers[1].ProviderName.c_str()); - Assert::AreEqual(secondProviderLevel, sourceEtw->Providers[1].Level); - Assert::AreEqual(secondProviderKeywords, sourceEtw->Providers[1].Keywords); - - // - // Check that guids are equal - // - LPWSTR providerGuid2 = NULL; - hr = StringFromCLSID(sourceEtw->Providers[1].ProviderGuid, &providerGuid2); - Assert::IsFalse(FAILED(hr)); - Assert::AreEqual(RemoveBracesGuidStr(secondProviderGuid).c_str(), RemoveBracesGuidStr(std::wstring(providerGuid2)).c_str()); - CoTaskMemFree(providerGuid2); - } - } - - /// - /// Test that default values for optional attributes on an etw source - /// are correct. - /// - TEST_METHOD(TestSourceETWDefaultValues) - { - HRESULT hr; - const static std::vector c_LevelToString = - { - L"Unknown", - L"Critical", - L"Error", - L"Warning", - L"Information", - L"Verbose", - }; - - - std::wstring firstProviderGuid = L"3A2A4E84-4C21-4981-AE10-3FDA0D9B0F83"; - - std::wstring configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"ETW\",\ - \"providers\" : [\ - {\ - \"providerGuid\": \"%s\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - firstProviderGuid.c_str() - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::ETW, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEtw = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(true, sourceEtw->EventFormatMultiLine); - - Assert::AreEqual((size_t)1, sourceEtw->Providers.size()); - - // - // First provider - // - Assert::AreEqual(L"", sourceEtw->Providers[0].ProviderName.c_str()); - Assert::AreEqual((UCHAR)2, sourceEtw->Providers[0].Level); // Error - Assert::AreEqual((ULONGLONG)0L, sourceEtw->Providers[0].Keywords); - - // - // Check that guids are equal - // - LPWSTR providerGuid1 = NULL; - hr = StringFromCLSID(sourceEtw->Providers[0].ProviderGuid, &providerGuid1); - Assert::IsFalse(FAILED(hr)); - Assert::AreEqual(RemoveBracesGuidStr(firstProviderGuid).c_str(), RemoveBracesGuidStr(std::wstring(providerGuid1)).c_str()); - CoTaskMemFree(providerGuid1); - } - - std::wstring firstProviderName = L"Microsoft-Windows-User-Diagnostic"; - - configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"ETW\",\ - \"providers\" : [\ - {\ - \"providerName\": \"%s\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - firstProviderName.c_str() - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::ETW, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEtw = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual(true, sourceEtw->EventFormatMultiLine); - - Assert::AreEqual((size_t)1, sourceEtw->Providers.size()); - - // - // First provider - // - Assert::AreEqual(firstProviderName.c_str(), sourceEtw->Providers[0].ProviderName.c_str()); - Assert::AreEqual((UCHAR)2, sourceEtw->Providers[0].Level); // Error - Assert::AreEqual((ULONGLONG)0L, sourceEtw->Providers[0].Keywords); - } - } - - /// - /// Test that ReadConfigFile reads attribute names in a case insensitive way. - /// - TEST_METHOD(TestCaseInsensitiveOnAttributeNames) - { - std::wstring configFileStr = - L"{ \ - \"logconfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"EventLog\",\ - \"channels\" : [\ - {\ - \"name\": \"system\",\ - \"level\" : \"Verbose\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::EventLog, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEventLog = std::reinterpret_pointer_cast(settings.Sources[0]); - - - Assert::AreEqual((size_t)1, sourceEventLog->Channels.size()); - - Assert::AreEqual(L"system", sourceEventLog->Channels[0].Name.c_str()); - Assert::AreEqual((int)EventChannelLogLevel::Verbose, (int)sourceEventLog->Channels[0].Level); - } - - configFileStr = - L"{ \ - \"LOGCONFIG\": { \ - \"SourCes\": [ \ - {\ - \"Type\": \"EventLog\",\ - \"CHANNELS\" : [\ - {\ - \"Name\": \"system\",\ - \"Level\" : \"Verbose\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::EventLog, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEventLog = std::reinterpret_pointer_cast(settings.Sources[0]); - - - Assert::AreEqual((size_t)1, sourceEventLog->Channels.size()); - - Assert::AreEqual(L"system", sourceEventLog->Channels[0].Name.c_str()); - Assert::AreEqual((int)EventChannelLogLevel::Verbose, (int)sourceEventLog->Channels[0].Level); - } - } - - /// - /// Test that bad formatted JSON strings throw errors. - /// - TEST_METHOD(TestInvalidJson) - { - std::wstring configFileStr; - - // - // Empty string. - // - configFileStr = - L""; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - std::function f1 = [&jsonParser, &settings] { bool success = ReadConfigFile(jsonParser, settings); }; - Assert::ExpectException(f1); - } - - // - // Invalid attribute name. - // - configFileStr = - L"{other: false}"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - std::function f1 = [&jsonParser, &settings] { bool success = ReadConfigFile(jsonParser, settings); }; - Assert::ExpectException(f1); - } - - // - // Invalid boolean value. - // - configFileStr = - L"{\"boolean\": Negative}"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - std::function f1 = [&jsonParser, &settings] { bool success = ReadConfigFile(jsonParser, settings); }; - Assert::ExpectException(f1); - } - - // - // Invalid numeric value. - // - configFileStr = - L"{\"numeric\": 0xff}"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - std::function f1 = [&jsonParser, &settings] { bool success = ReadConfigFile(jsonParser, settings); }; - Assert::ExpectException(f1); - } - - // - // Invalid escape sequence. - // - configFileStr = - L"{\"text\": \"\\k\"}"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - std::function f1 = [&jsonParser, &settings] { bool success = ReadConfigFile(jsonParser, settings); }; - Assert::ExpectException(f1); - } - - // - // Expected next element on an object. - // - configFileStr = - L"{\"text\": \"\",}"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - std::function f1 = [&jsonParser, &settings] { bool success = ReadConfigFile(jsonParser, settings); }; - Assert::ExpectException(f1); - } - - // - // Expected next element on an array. - // - configFileStr = - L"{\"array\":[\"text\": \"\",]}"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - std::function f1 = [&jsonParser, &settings] { bool success = ReadConfigFile(jsonParser, settings); }; - Assert::ExpectException(f1); - } - } - - /// - /// Test that valid JSON strings, but invalid values for a configuration string, - /// return false when passed to ReadConfigFile. - /// - TEST_METHOD(TestInvalidConfigFile) - { - std::wstring configFileStr; - - // - // 'LogConfig' root element doesn't exist. - // - configFileStr = - L"{\"other\": { }}"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsFalse(success); - } - - // - // LogConfig is an array. - // - configFileStr = - L"{ \ - \"LogConfig\": []\ - }"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsFalse(success); - } - - // - // 'Sources' doesn't exist. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"other\": [ \ - {\ - \"type\": \"File\",\ - \"directory\": \"C:\\\\logs\"\ - }\ - ]\ - }\ - }"; - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsFalse(success); - } - - // - // 'sources' isn't an array. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": \ - {\ - \"type\": \"File\",\ - \"directory\": \"C:\\\\logs\"\ - }\ - }\ - }"; - - { - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsFalse(success); - } - - // - // Check a source with invalid type. - // This case is special, because it shouldn't return false, but a - // warning message should be throw. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"Unknown\",\ - \"directory\": \"C:\\\\logs\"\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::IsTrue(output.find(L"ERROR") != std::wstring::npos || output.find(L"WARNING") != std::wstring::npos); - } - } - - /// - /// Check that invalid EventLog sources are not returned by ReadConfigFile. - /// - TEST_METHOD(TestInvalidEventLogSource) - { - std::wstring configFileStr; - - // - // 'Channels' doesn't exist. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"EventLog\",\ - \"other\" : [\ - {\ - \"name\": \"system\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)0, settings.Sources.size()); - Assert::IsTrue(output.find(L"ERROR") != std::wstring::npos || output.find(L"WARNING") != std::wstring::npos); - } - - // - // 'Channels' isn't an array. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"EventLog\",\ - \"channels\" : \ - {\ - \"name\": \"system\"\ - }\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)0, settings.Sources.size()); - Assert::IsTrue(output.find(L"ERROR") != std::wstring::npos || output.find(L"WARNING") != std::wstring::npos); - } - - // - // Invalid channel. It should have at least a 'name' attribute. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"EventLog\",\ - \"channels\" : [\ - {\ - \"other\": \"system\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::EventLog, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEventLog = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual((size_t)0, sourceEventLog->Channels.size()); - Assert::IsTrue(output.find(L"WARNING") != std::wstring::npos); - } - - // - // Invalid level. - // This case is special, because the source should be returned, but a - // warning message should be throw. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"EventLog\",\ - \"channels\" : [\ - {\ - \"name\": \"system\",\ - \"level\": \"Invalid\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::EventLog, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceEventLog = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual((size_t)1, sourceEventLog->Channels.size()); - Assert::AreEqual((int)EventChannelLogLevel::Error, (int)sourceEventLog->Channels[0].Level); - Assert::IsTrue(output.find(L"WARNING") != std::wstring::npos); - } - } - - /// - /// Check that invalid File sources are not returned by ReadConfigFile. - /// - TEST_METHOD(TestInvalidFileSource) - { - std::wstring configFileStr; - - // - // 'Directory' doesn't exist - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"File\",\ - \"other\": \"C:\\\\logs\"\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)0, settings.Sources.size()); - Assert::IsTrue(output.find(L"ERROR") != std::wstring::npos || output.find(L"WARNING") != std::wstring::npos); - } - } - - TEST_METHOD(TestRootDirectoryConfigurations) - { - - const std::wstring directory = L"C:\\"; - bool includeSubdirectories = false; - - std::wstring configFileStr; - std::wstring configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"File\",\ - \"directory\": \"%s\",\ - \"includeSubdirectories\": %s\ - }\ - ]\ - }\ - }"; - - // Valid: Root dir and includeSubdirectories = false - { - configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - Utility::ReplaceAll(directory, L"\\", L"\\\\").c_str(), - includeSubdirectories ? L"true" : L"false"); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - Assert::AreEqual(L"", output.c_str()); - } - - // Invalid: Root dir and includeSubdirectories = true - { - includeSubdirectories = true; - configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - Utility::ReplaceAll(directory, L"\\", L"\\\\").c_str(), - includeSubdirectories ? L"true" : L"false"); - - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - Assert::IsTrue(output.find(L"WARNING") != std::wstring::npos); - } - } - - /// - /// Check that invalid ETW sources are not returned by ReadConfigFile. - /// - TEST_METHOD(TestInvalidETWSource) - { - std::wstring configFileStr; - - // - // 'providers' doesn't exist - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"ETW\",\ - \"other\" : [\ - {\ - \"providerGuid\": \"305FC87B-002A-5E26-D297-60223012CA9C\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)0, settings.Sources.size()); - Assert::IsTrue(output.find(L"ERROR") != std::wstring::npos || output.find(L"WARNING") != std::wstring::npos); - } - - // - // Invalid provider - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"ETW\",\ - \"providers\" : [\ - {\ - \"level\": \"Information\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::ETW, (int)settings.Sources[0]->Type); - - std::shared_ptr SourceEtw = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual((size_t)0, SourceEtw->Providers.size()); - Assert::IsTrue(output.find(L"WARNING") != std::wstring::npos); - } - - // - // Invalid providerGuid. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"ETW\",\ - \"providers\" : [\ - {\ - \"providerGuid\": \"305FC87B-002A-5E26-D297-60\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::ETW, (int)settings.Sources[0]->Type); - - std::shared_ptr SourceEtw = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual((size_t)0, SourceEtw->Providers.size()); - Assert::IsTrue(output.find(L"WARNING") != std::wstring::npos); - } - - // - // Invalid level. - // This case is special, because the source should be returned, but a - // warning message should be throw. - // - configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"ETW\",\ - \"providers\" : [\ - {\ - \"providerGuid\": \"305FC87B-002A-5E26-D297-60223012CA9C\",\ - \"level\": \"Info\"\ - }\ - ]\ - }\ - ]\ - }\ - }"; - - { - fflush(stdout); - ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - Assert::IsTrue(success); - - std::wstring output = RecoverOuput(); - - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::ETW, (int)settings.Sources[0]->Type); - - std::shared_ptr SourceEtw = std::reinterpret_pointer_cast(settings.Sources[0]); - - Assert::AreEqual((size_t)1, SourceEtw->Providers.size()); - Assert::AreEqual((UCHAR)2, SourceEtw->Providers[0].Level); // Error - Assert::IsTrue(output.find(L"WARNING") != std::wstring::npos); - } - } - - /// - /// Check that UTF8 encoded config file is opened and read by OpenConfigFile. - /// - TEST_METHOD(TestUTF8EncodedConfigFileReading) - { - //create a temp folder to hold config - std::wstring tempDirectory = CreateTempDirectory(); - Assert::IsFalse(tempDirectory.empty()); - directoriesToDeleteAtCleanup.push_back(tempDirectory); - // - // Create subdirectory - // - std::wstring subDirectory = tempDirectory + L"\\LogMonitor"; - long status = CreateDirectoryW(subDirectory.c_str(), NULL); - Assert::AreNotEqual(0L, status); - - std::wstring fileName = L"LogMonitorConfigTesting.json"; - std::wstring fullFileName = subDirectory + L"\\" + fileName; - - //create the utf8 encoded config file - std::wstring configFileStr = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - ]\ - }\ - }"; - - std::wofstream wof; - wof.imbue(std::locale(std::locale::empty(), new std::codecvt_utf8)); - wof.open(fullFileName); - wof << configFileStr; - wof.close(); - - //check if the file can be successfully read by OpenConfigFile - LoggerSettings settings; - bool succcess = OpenConfigFile((PWCHAR)fullFileName.c_str(), settings); - Assert::AreEqual(succcess, true); - } - - TEST_METHOD(TestWaitInSeconds){ - // Test WaitInSeconds input as value - TestWaitInSecondsValues(L"242", false); - - // Test WaitInSeconds input as string - TestWaitInSecondsValues(L"359", true); - - // Test WaitInSeconds input is Infinity - TestWaitInSecondsValues(L"INFINITY", true); - } - - TEST_METHOD(TestInvalidWaitInSeconds) { - std::wstring directory = L"C:\\LogMonitor\\logs"; - TestInvalidWaitInSecondsValues(L"-10", false); - TestInvalidWaitInSecondsValues(L"-Inf", true); - } - - private: - void TestWaitInSecondsValues(std::wstring waitInSeconds, bool asString = false) { - std::wstring directory = L"C:\\LogMonitor\\logs"; - std::wstring configFileStr = GetConfigFileStrFormat(directory, waitInSeconds, asString); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - // - // The config string was valid - // - Assert::IsTrue(success); - - // - // The source Event Log is valid - // - Assert::AreEqual((size_t)1, settings.Sources.size()); - Assert::AreEqual((int)LogSourceType::File, (int)settings.Sources[0]->Type); - - std::shared_ptr sourceFile = std::reinterpret_pointer_cast(settings.Sources[0]); - - if (isinf(std::stod(waitInSeconds))) { - Assert::IsTrue(isinf(sourceFile->WaitInSeconds)); - } - else { - double precision = 1e-6; - Assert::AreEqual(std::stod(waitInSeconds), sourceFile->WaitInSeconds, precision); - } - } - - void TestInvalidWaitInSecondsValues(std::wstring waitInSeconds, bool asString = false) { - std::wstring directory = L"C:\\LogMonitor\\logs"; - std::wstring configFileStr = GetConfigFileStrFormat(directory, waitInSeconds, asString); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - Assert::IsTrue(success); - Assert::IsTrue(output.find(L"WARNING") != std::wstring::npos); - - Assert::IsTrue(success); - Assert::IsTrue(output.find(L"WARNING") != std::wstring::npos); - } - - std::wstring GetConfigFileStrFormat(std::wstring directory, std::wstring waitInSeconds, bool asString) { - std::wstring configFileStrFormat; - if (asString) { - configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"File\",\ - \"directory\": \"%s\",\ - \"waitInSeconds\": \"%s\"\ - }\ - ]\ - }\ - }"; - - return Utility::FormatString( - configFileStrFormat.c_str(), - Utility::ReplaceAll(directory, L"\\", L"\\\\").c_str(), - waitInSeconds.c_str() - ); - } - else { - configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"sources\": [ \ - {\ - \"type\": \"File\",\ - \"directory\": \"%s\",\ - \"waitInSeconds\": %f\ - }\ - ]\ - }\ - }"; - return Utility::FormatString( - configFileStrFormat.c_str(), - Utility::ReplaceAll(directory, L"\\", L"\\\\").c_str(), - std::stod(waitInSeconds) - ); - } - } - - TEST_METHOD(TestSourceProcess) - { - // - // Template of a valid configuration string, with a process source. - // - std::wstring configFileStrFormat = - L"{ \ - \"LogConfig\": { \ - \"logFormat\": \"%s\",\ - \"sources\": [ \ - {\ - \"type\": \"Process\",\ - \"customLogFormat\": \"%s\"\ - }\ - ]\ - }\ - }"; - - std::wstring logFormat = L"custom"; - std::wstring customLogFormat = L"{'TimeStamp':'%TimeStamp%', 'source':'%Source%', 'Message':'%Message%'}"; - { - std::wstring configFileStr = Utility::FormatString( - configFileStrFormat.c_str(), - logFormat.c_str(), - customLogFormat.c_str() - ); - - JsonFileParser jsonParser(configFileStr); - LoggerSettings settings; - - bool success = ReadConfigFile(jsonParser, settings); - - std::wstring output = RecoverOuput(); - - // - // The config string was valid - // - Assert::IsTrue(success); - Assert::AreEqual(L"", output.c_str()); - } - } - - }; -} diff --git a/LogMonitor/LogMonitorTests/JsonProcessorTests.cpp b/LogMonitor/LogMonitorTests/JsonProcessorTests.cpp new file mode 100644 index 00000000..5c223e72 --- /dev/null +++ b/LogMonitor/LogMonitorTests/JsonProcessorTests.cpp @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +#include "pch.h" + + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +#define BUFFER_SIZE 65536 + +namespace UtilityTests +{ + /// + /// Tests of Utility class methods. + /// + TEST_CLASS(JsonProcessorTests) + { + public: + + }; +} diff --git a/LogMonitor/LogMonitorTests/LogMonitorTests.cpp b/LogMonitor/LogMonitorTests/LogMonitorTests.cpp index 5aba0f2d..2150ea4b 100644 --- a/LogMonitor/LogMonitorTests/LogMonitorTests.cpp +++ b/LogMonitor/LogMonitorTests/LogMonitorTests.cpp @@ -5,7 +5,6 @@ #include "pch.h" -#include "../src/LogMonitor/ConfigFileParser.cpp" #include "../src/LogMonitor/EtwMonitor.cpp" #include "../src/LogMonitor/EventMonitor.cpp" #include "../src/LogMonitor/JsonFileParser.cpp" diff --git a/LogMonitor/LogMonitorTests/LogMonitorTests.vcxproj b/LogMonitor/LogMonitorTests/LogMonitorTests.vcxproj index 23dffda2..32929bda 100644 --- a/LogMonitor/LogMonitorTests/LogMonitorTests.vcxproj +++ b/LogMonitor/LogMonitorTests/LogMonitorTests.vcxproj @@ -166,6 +166,7 @@ + @@ -174,7 +175,6 @@ Create Create - diff --git a/LogMonitor/LogMonitorTests/LogMonitorTests.vcxproj.filters b/LogMonitor/LogMonitorTests/LogMonitorTests.vcxproj.filters index 26c604af..25533dc9 100644 --- a/LogMonitor/LogMonitorTests/LogMonitorTests.vcxproj.filters +++ b/LogMonitor/LogMonitorTests/LogMonitorTests.vcxproj.filters @@ -21,9 +21,6 @@ Source Files - - Source Files - Source Files @@ -38,6 +35,9 @@ Source Files + + + Source Files diff --git a/LogMonitor/src/CMakeLists.txt b/LogMonitor/src/CMakeLists.txt new file mode 100644 index 00000000..8f570a9d --- /dev/null +++ b/LogMonitor/src/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.15) + +project(LogMonitor) + +find_package(Boost REQUIRED COMPONENTS json) + +file(GLOB_RECURSE SourceFiles RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*.cpp") + +# Define LogMonitor as a library +add_library(LogMonitor ${SourceFiles}) + +# Add precompiled headers (PCH) +target_precompile_headers(LogMonitor PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor/pch.h) + +# Include directories for LogMonitor +target_include_directories(LogMonitor PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor + ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor/FileMonitor + ${Boost_INCLUDE_DIRS} +) + +# Link Boost JSON to LogMonitor +target_link_libraries(LogMonitor PRIVATE Boost::json) diff --git a/LogMonitor/src/LogMonitor/ConfigFileParser.cpp b/LogMonitor/src/LogMonitor/ConfigFileParser.cpp deleted file mode 100644 index c29a835a..00000000 --- a/LogMonitor/src/LogMonitor/ConfigFileParser.cpp +++ /dev/null @@ -1,774 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// - -#include "pch.h" -#include "./Parser/ConfigFileParser.h" -#include "./LogWriter.h" -#include "./FileMonitor/FileMonitorUtilities.h" - -/// ConfigFileParser.cpp -/// -/// Reads the configuration file content (as a string), parsing it with a -/// JsonFileParser object previously created. -/// -/// The main entry point in this file is OpenConfigFile. -/// - -/// -/// Open the config file and convert the document content into json -/// -/// \param FileName Config File name. -/// -/// \return True if the configuration file was valid. Otherwise false -/// -bool OpenConfigFile(_In_ const PWCHAR ConfigFileName, _Out_ LoggerSettings& Config) -{ - bool success; - std::wifstream configFileStream(ConfigFileName); - configFileStream.imbue(std::locale(configFileStream.getloc(), - new std::codecvt_utf8_utf16)); - - if (configFileStream.is_open()) - { - try - { - // - // Convert the document content to a string, to pass it to JsonFileParser constructor. - // - std::wstring configFileStr((std::istreambuf_iterator(configFileStream)), - std::istreambuf_iterator()); - configFileStr.erase(remove(configFileStr.begin(), configFileStr.end(), 0xFEFF), configFileStr.end()); - - JsonFileParser jsonParser(configFileStr); - - success = ReadConfigFile(jsonParser, Config); - } - catch (std::exception& ex) - { - logWriter.TraceError( - Utility::FormatString(L"Failed to read json configuration file. %S", ex.what()).c_str() - ); - success = false; - } - catch (...) - { - logWriter.TraceError( - Utility::FormatString(L"Failed to read json configuration file. Unknown error occurred.").c_str() - ); - success = false; - } - } else { - logWriter.TraceError( - Utility::FormatString( - L"Configuration file '%s' not found. Logs will not be monitored.", - ConfigFileName - ).c_str() - ); - success = false; - } - - return success; -} - -/// -/// Read the root JSON of the config file -/// -/// \param Parser A pre-initialized JSON parser. -/// \param Config Returns a LoggerSettings struct with the values specified -/// in the config file. -/// -/// \return True if the configuration file was valid. Otherwise false -/// -bool -ReadConfigFile( - _In_ JsonFileParser& Parser, - _Out_ LoggerSettings& Config - ) -{ - if (Parser.GetNextDataType() != JsonFileParser::DataType::Object) - { - logWriter.TraceError(L"Failed to parse configuration file. Object expected at the file's root"); - return false; - } - - bool containsLogConfigTag = false; - - if (Parser.BeginParseObject()) - { - do - { - const std::wstring key(Parser.GetKey()); - - if (_wcsnicmp(key.c_str(), JSON_TAG_LOG_CONFIG, _countof(JSON_TAG_LOG_CONFIG)) == 0) - { - containsLogConfigTag = ReadLogConfigObject(Parser, Config); - } - else - { - Parser.SkipValue(); - } - } while (Parser.ParseNextObjectElement()); - } - - return containsLogConfigTag; -} - -/// -/// Read LogConfig tag, that contains the config of the sources -/// -/// \param Parser A pre-initialized JSON parser. -/// \param Config Returns a LoggerSettings struct with the values specified -/// in the config file. -/// -/// \return True if LogConfig tag was valid. Otherwise false -/// -bool -ReadLogConfigObject( - _In_ JsonFileParser& Parser, - _Out_ LoggerSettings& Config - ) -{ - if (Parser.GetNextDataType() != JsonFileParser::DataType::Object) - { - logWriter.TraceError(L"Failed to parse configuration file. 'LogConfig' is expected to be an object"); - Parser.SkipValue(); - return false; - } - - bool sourcesTagFound = false; - if (Parser.BeginParseObject()) - { - std::wstring key; - do - { - key = Parser.GetKey(); - - if (_wcsnicmp(key.c_str(), JSON_TAG_SOURCES, _countof(JSON_TAG_SOURCES)) == 0) - { - if (Parser.GetNextDataType() != JsonFileParser::DataType::Array) - { - logWriter.TraceError(L"Failed to parse configuration file. 'sources' attribute expected to be an array"); - Parser.SkipValue(); - continue; - } - - sourcesTagFound = true; - - if (!Parser.BeginParseArray()) - { - continue; - } - - do - { - AttributesMap sourceAttributes; - - // - // Read all attributes of a source from the config file, - // instantiate it and add it to the end of the vector - // - if (ReadSourceAttributes(Parser, sourceAttributes)) { - if (!AddNewSource(Parser, sourceAttributes, Config.Sources)) - { - logWriter.TraceWarning(L"Failed to parse configuration file. Error reading invalid source."); - } - } - else - { - logWriter.TraceWarning(L"Failed to parse configuration file. Error retrieving source attributes. Invalid source"); - } - - for (auto attributePair : sourceAttributes) - { - if (attributePair.second != nullptr) - { - delete attributePair.second; - } - } - } while (Parser.ParseNextArrayElement()); - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_LOG_FORMAT, _countof(JSON_TAG_LOG_FORMAT)) == 0) - { - Config.LogFormat = std::wstring(Parser.ParseStringValue()); - } - else - { - logWriter.TraceWarning(Utility::FormatString(L"Error parsing configuration file. 'Unknow key %ws in the configuration file.", key.c_str()).c_str()); - Parser.SkipValue(); - } - } while (Parser.ParseNextObjectElement()); - } - - return sourcesTagFound; -} - -/// -/// Look for all the attributes that a single 'source' object contains -/// -/// \param Parser A pre-initialized JSON parser. -/// \param Config Returns an AttributesMap, with all allowed tag names and theirs values -/// -/// \return True if the attributes contained valid values. Otherwise false -/// -bool -ReadSourceAttributes( - _In_ JsonFileParser& Parser, - _Out_ AttributesMap& Attributes - ) -{ - if (Parser.GetNextDataType() != JsonFileParser::DataType::Object) - { - logWriter.TraceError(L"Failed to parse configuration file. Source item expected to be an object"); - Parser.SkipValue(); - return false; - } - - bool success = true; - - if (Parser.BeginParseObject()) - { - do - { - // - // If source reading already fail, just skip attributes^M - // - if (!success) - { - Parser.SkipValue(); - continue; - } - - const std::wstring key(Parser.GetKey()); - - if (_wcsnicmp(key.c_str(), JSON_TAG_TYPE, _countof(JSON_TAG_TYPE)) == 0) - { - const auto& typeString = Parser.ParseStringValue(); - LogSourceType* type = nullptr; - - // - // Check if the string is the name of a valid LogSourceType - // - int sourceTypeArraySize = sizeof(LogSourceTypeNames) / sizeof(LogSourceTypeNames[0]); - for (int i = 0; i < sourceTypeArraySize; i++) - { - if (_wcsnicmp(typeString.c_str(), LogSourceTypeNames[i], typeString.length()) == 0) - { - type = new LogSourceType; - *type = static_cast(i); - } - } - - // - // If the value isn't a valid type, fail. - // - if (type == nullptr) - { - logWriter.TraceError( - Utility::FormatString( - L"Error parsing configuration file. '%s' isn't a valid source type", typeString.c_str() - ).c_str() - ); - - success = false; - } - else - { - Attributes[key] = type; - } - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_CHANNELS, _countof(JSON_TAG_CHANNELS)) == 0) - { - if (Parser.GetNextDataType() != JsonFileParser::DataType::Array) - { - logWriter.TraceError(L"Error parsing configuration file. 'channels' attribute expected to be an array"); - Parser.SkipValue(); - continue; - } - - if (Parser.BeginParseArray()) - { - std::vector* channels = new std::vector(); - - // - // Get only the valid channels of this JSON object. - // - do - { - channels->emplace_back(); - if (!ReadLogChannel(Parser, channels->back())) - { - logWriter.TraceWarning(L"Error parsing configuration file. Discarded invalid channel (it must have a non-empty 'name')."); - channels->pop_back(); - } - } while (Parser.ParseNextArrayElement()); - - Attributes[key] = channels; - } - } - // - // These attributes are string type - // * directory - // * filter - // * lineLogFormat - // - else if (_wcsnicmp(key.c_str(), JSON_TAG_DIRECTORY, _countof(JSON_TAG_DIRECTORY)) == 0) - { - std::wstring directory = Parser.ParseStringValue(); - FileMonitorUtilities::ParseDirectoryValue(directory); - Attributes[key] = new std::wstring(directory); - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_FILTER, _countof(JSON_TAG_FILTER)) == 0 - || _wcsnicmp(key.c_str(), JSON_TAG_CUSTOM_LOG_FORMAT, _countof(JSON_TAG_CUSTOM_LOG_FORMAT)) == 0) - { - Attributes[key] = new std::wstring(Parser.ParseStringValue()); - } - // - // These attributes are boolean type - // * eventFormatMultiLine - // * startAtOldestRecord - // * includeSubdirectories - // - else if ( - _wcsnicmp( - key.c_str(), - JSON_TAG_FORMAT_MULTILINE, - _countof(JSON_TAG_FORMAT_MULTILINE)) == 0 - || _wcsnicmp( - key.c_str(), - JSON_TAG_START_AT_OLDEST_RECORD, - _countof(JSON_TAG_START_AT_OLDEST_RECORD)) == 0 - || _wcsnicmp( - key.c_str(), - JSON_TAG_INCLUDE_SUBDIRECTORIES, - _countof(JSON_TAG_INCLUDE_SUBDIRECTORIES)) == 0 - ) - { - Attributes[key] = new bool{ Parser.ParseBooleanValue() }; - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_PROVIDERS, _countof(JSON_TAG_PROVIDERS)) == 0) - { - if (Parser.GetNextDataType() != JsonFileParser::DataType::Array) - { - logWriter.TraceError(L"Error parsing configuration file. 'providers' attribute expected to be an array"); - Parser.SkipValue(); - continue; - } - - if (Parser.BeginParseArray()) - { - std::vector* providers = new std::vector(); - - // - // Get only the valid providers of this JSON object. - // - do - { - providers->emplace_back(); - if (!ReadETWProvider(Parser, providers->back())) - { - logWriter.TraceWarning(L"Error parsing configuration file. Discarded invalid provider (it must have a non-empty 'providerName' or 'providerGuid')."); - providers->pop_back(); - } - } while (Parser.ParseNextArrayElement()); - - Attributes[key] = providers; - } - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_WAITINSECONDS, _countof(JSON_TAG_WAITINSECONDS)) == 0) - { - try - { - auto parsedValue = new std::double_t(Parser.ParseNumericValue()); - if (*parsedValue < 0) - { - logWriter.TraceError(L"Error parsing configuration file. 'waitInSeconds' attribute must be greater or equal to zero"); - success = false; - } - else - { - Attributes[key] = parsedValue; - } - } - catch(const std::exception& ex) - { - logWriter.TraceError( - Utility::FormatString(L"Error parsing configuration file atrribute 'waitInSeconds'. %S", ex.what()).c_str() - ); - success = false; - } - } - else - { - // - // Discard unwanted attributes - // - Parser.SkipValue(); - } - } while (Parser.ParseNextObjectElement()); - } - - bool isSourceFileValid = ValidateDirectoryAttributes(Attributes); - if (!isSourceFileValid) - { - success = false; - } - - return success; -} - -/// -/// Reads a single 'channel' object from the parser, and store it in the Result -/// -/// \param Parser A pre-initialized JSON parser. -/// \param Result Returns an EventLogChannel struct filled with the values specified in the config file. -/// -/// \return True if the channel is valid. Otherwise false -/// -bool -ReadLogChannel( - _In_ JsonFileParser& Parser, - _Out_ EventLogChannel& Result - ) -{ - if (Parser.GetNextDataType() != JsonFileParser::DataType::Object) - { - logWriter.TraceError(L"Error parsing configuration file. Channel item expected to be an object"); - Parser.SkipValue(); - return false; - } - - if (!Parser.BeginParseObject()) - { - logWriter.TraceError(L"Error parsing configuration file. Error reading channel object"); - return false; - } - - do - { - const auto& key = Parser.GetKey(); - - if (_wcsnicmp(key.c_str(), JSON_TAG_CHANNEL_NAME, _countof(JSON_TAG_CHANNEL_NAME)) == 0) - { - // - // Recover the name of the channel - // - Result.Name = Parser.ParseStringValue(); - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_CHANNEL_LEVEL, _countof(JSON_TAG_CHANNEL_LEVEL)) == 0) - { - // - // Get the level as a string, and convert it to EventChannelLogLevel. - // - std::wstring logLevelStr = Parser.ParseStringValue(); - bool success = Result.SetLevelByString(logLevelStr); - - // - // Print an error message the string doesn't matches with a valid log level name. - // - if (!success) - { - logWriter.TraceWarning( - Utility::FormatString( - L"Error parsing configuration file. '%s' isn't a valid log level. Setting 'Error' level as default", logLevelStr.c_str() - ).c_str() - ); - } - } - else - { - // - // Discard unwanted attributes - // - Parser.SkipValue(); - } - } while (Parser.ParseNextObjectElement()); - - - return Result.IsValid(); -} - -/// -/// Reads a single 'provider' object from the parser, and return it in the Result param -/// -/// \param Parser A pre-initialized JSON parser. -/// \param Result Returns an ETWProvider struct filled with the values specified in the config file. -/// -/// \return True if the channel is valid. Otherwise false -/// -bool -ReadETWProvider( - _In_ JsonFileParser& Parser, - _Out_ ETWProvider& Result - ) -{ - if (Parser.GetNextDataType() != JsonFileParser::DataType::Object) - { - logWriter.TraceError(L"Error parsing configuration file. Provider item expected to be an object"); - Parser.SkipValue(); - return false; - } - - if (!Parser.BeginParseObject()) - { - logWriter.TraceError(L"Error parsing configuration file. Error reading provider object"); - return false; - } - - do - { - const auto& key = Parser.GetKey(); - - if (_wcsnicmp(key.c_str(), JSON_TAG_PROVIDER_NAME, _countof(JSON_TAG_PROVIDER_NAME)) == 0) - { - // - // Recover the name of the provider - // - Result.ProviderName = Parser.ParseStringValue(); - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_PROVIDER_GUID, _countof(JSON_TAG_PROVIDER_GUID)) == 0) - { - // - // Recover the GUID of the provider. If the string isn't a valid - // GUID, ProviderGuidStr will be empty - // - Result.SetProviderGuid(Parser.ParseStringValue()); - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_PROVIDER_LEVEL, _countof(JSON_TAG_PROVIDER_LEVEL)) == 0) - { - // - // Get the level as a string, and convert it to EventChannelLogLevel. - // - std::wstring logLevelStr = Parser.ParseStringValue(); - bool success = Result.StringToLevel(logLevelStr); - - // - // Print an error message the string doesn't matches with a valid log level name. - // - if (!success) - { - logWriter.TraceWarning( - Utility::FormatString( - L"Error parsing configuration file. '%s' isn't a valid log level. Setting 'Error' level as default", logLevelStr.c_str() - ).c_str() - ); - } - } - else if (_wcsnicmp(key.c_str(), JSON_TAG_KEYWORDS, _countof(JSON_TAG_KEYWORDS)) == 0) - { - // - // Recover the GUID of the provider - // - Result.Keywords = wcstoull(Parser.ParseStringValue().c_str(), NULL, 0); - } - else - { - // - // Discard unwanted attributes - // - Parser.SkipValue(); - } - } while (Parser.ParseNextObjectElement()); - - - return Result.IsValid(); -} - -/// -/// Converts the attributes to a valid Source and add it to the vector Sources -/// -/// \param Parser A pre-initialized JSON parser. -/// \param Attributes An AttributesMap that contains the attributes of the new source objet. -/// \param Sources A vector, where the new source is going to be inserted after been instantiated. -/// -/// \return True if Source was created and added successfully. Otherwise false -/// -bool -AddNewSource( - _In_ JsonFileParser& Parser, - _In_ AttributesMap& Attributes, - _Inout_ std::vector >& Sources - ) -{ - // - // Check the source has a type. - // - if (Attributes.find(JSON_TAG_TYPE) == Attributes.end() - || Attributes[JSON_TAG_TYPE] == nullptr) - { - return false; - } - - switch (*(LogSourceType*)Attributes[JSON_TAG_TYPE]) - { - case LogSourceType::EventLog: - { - std::shared_ptr sourceEventLog = std::make_shared< SourceEventLog>(); - - // - // Fill the new EventLog source object, with its attributes - // - if (!SourceEventLog::Unwrap(Attributes, *sourceEventLog)) - { - logWriter.TraceError(L"Error parsing configuration file. Invalid EventLog source (it must have a non-empty 'channels')"); - return false; - } - - Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceEventLog))); - - break; - } - - case LogSourceType::File: - { - std::shared_ptr sourceFile = std::make_shared< SourceFile>(); - - // - // Fill the new File source object, with its attributes - // - if (!SourceFile::Unwrap(Attributes, *sourceFile)) - { - logWriter.TraceError(L"Error parsing configuration file. Invalid File source (it must have a non-empty 'directory')"); - return false; - } - - Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceFile))); - - break; - } - - case LogSourceType::ETW: - { - std::shared_ptr sourceETW = std::make_shared< SourceETW>(); - - // - // Fill the new ETW source object, with its attributes - // - if (!SourceETW::Unwrap(Attributes, *sourceETW)) - { - logWriter.TraceError(L"Error parsing configuration file. Invalid ETW source (it must have a non-empty 'providers')"); - return false; - } - - Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceETW))); - - break; - } - - case LogSourceType::Process: - { - std::shared_ptr sourceProcess = std::make_shared< SourceProcess>(); - - if (!SourceProcess::Unwrap(Attributes, *sourceProcess)) - { - logWriter.TraceError(L"Error parsing configuration file. Invalid Process source)"); - return false; - } - - Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceProcess))); - - break; - } - } - return true; -} - -/// -/// Validates that when root directory is passed, includeSubdirectories is false -/// -/// \param Attributes An AttributesMap that contains the attributes of the new source objet. -/// \return false when root directory is passed, includeSubdirectories = true. Otherwise, true -bool ValidateDirectoryAttributes(_In_ AttributesMap &Attributes) -{ - if (!Utility::ConfigAttributeExists(Attributes, JSON_TAG_DIRECTORY) || - !Utility::ConfigAttributeExists(Attributes, JSON_TAG_INCLUDE_SUBDIRECTORIES)) - { - return true; - } - - std::wstring directory = *(std::wstring *)Attributes[JSON_TAG_DIRECTORY]; - const bool includeSubdirectories = *(bool *)Attributes[JSON_TAG_INCLUDE_SUBDIRECTORIES]; - - // Check if Log file monitor config is valid - const bool isValid = FileMonitorUtilities::IsValidSourceFile(directory, includeSubdirectories); - if (!isValid) - { - logWriter.TraceError( - Utility::FormatString( - L"LoggerSettings: Invalid Source File atrribute 'directory' (%s) and 'includeSubdirectories' (%s)." - L"'includeSubdirectories' attribute cannot be 'true' for the root directory", - directory.c_str(), includeSubdirectories ? L"true" : L"false") - .c_str()); - } - return isValid; -} - -/// -/// Debug function -/// -void _PrintSettings(_Out_ LoggerSettings& Config) -{ - std::wprintf(L"LogConfig:\n"); - std::wprintf(L"\tsources:\n"); - - for (auto source : Config.Sources) - { - switch (source->Type) - { - case LogSourceType::EventLog: - { - std::wprintf(L"\t\tType: EventLog\n"); - std::shared_ptr sourceEventLog = std::reinterpret_pointer_cast(source); - - std::wprintf(L"\t\teventFormatMultiLine: %ls\n", sourceEventLog->EventFormatMultiLine ? L"true" : L"false"); - std::wprintf(L"\t\tstartAtOldestRecord: %ls\n", sourceEventLog->StartAtOldestRecord ? L"true" : L"false"); - - std::wprintf(L"\t\tChannels (%d):\n", (int)sourceEventLog->Channels.size()); - for (auto channel : sourceEventLog->Channels) - { - std::wprintf(L"\t\t\tName: %ls\n", channel.Name.c_str()); - std::wprintf(L"\t\t\tLevel: %d\n", (int)channel.Level); - std::wprintf(L"\n"); - } - std::wprintf(L"\n"); - - break; - } - case LogSourceType::File: - { - std::wprintf(L"\t\tType: File\n"); - std::shared_ptr sourceFile = std::reinterpret_pointer_cast(source); - - std::wprintf(L"\t\tDirectory: %ls\n", sourceFile->Directory.c_str()); - std::wprintf(L"\t\tFilter: %ls\n", sourceFile->Filter.c_str()); - std::wprintf(L"\t\tIncludeSubdirectories: %ls\n", sourceFile->IncludeSubdirectories ? L"true" : L"false"); - std::wprintf(L"\t\twaitInSeconds: %d\n", int(sourceFile->WaitInSeconds)); - std::wprintf(L"\n"); - - break; - } - case LogSourceType::ETW: - { - std::wprintf(L"\t\tType: ETW\n"); - - std::shared_ptr sourceETW = std::reinterpret_pointer_cast(source); - - std::wprintf(L"\t\teventFormatMultiLine: %ls\n", sourceETW->EventFormatMultiLine ? L"true" : L"false"); - - std::wprintf(L"\t\tProviders (%d):\n", (int)sourceETW->Providers.size()); - for (auto provider : sourceETW->Providers) - { - std::wprintf(L"\t\t\tProviderName: %ls\n", provider.ProviderName.c_str()); - std::wprintf(L"\t\t\tProviderGuid: %ls\n", provider.ProviderGuidStr.c_str()); - std::wprintf(L"\t\t\tLevel: %d\n", (int)provider.Level); - std::wprintf(L"\t\t\tKeywords: %llx\n", provider.Keywords); - std::wprintf(L"\n"); - } - std::wprintf(L"\n"); - - break; - } - } // Switch - } -} diff --git a/LogMonitor/src/LogMonitor/EtwMonitor.cpp b/LogMonitor/src/LogMonitor/EtwMonitor.cpp index 98fc3c03..6630e483 100644 --- a/LogMonitor/src/LogMonitor/EtwMonitor.cpp +++ b/LogMonitor/src/LogMonitor/EtwMonitor.cpp @@ -663,7 +663,7 @@ EtwMonitor::OnRecordEvent( } catch (std::bad_alloc& e) { logWriter.TraceError( - Utility::FormatString(L"Failed to allocate memory for event info (size=%lu).", bufferSize).c_str() + Utility::FormatString(L"Failed to allocate memory for event info (size=%lu): %s", bufferSize, e.what()).c_str() ); status = ERROR_OUTOFMEMORY; } diff --git a/LogMonitor/src/LogMonitor/JsonProcessor.cpp b/LogMonitor/src/LogMonitor/JsonProcessor.cpp new file mode 100644 index 00000000..b962b669 --- /dev/null +++ b/LogMonitor/src/LogMonitor/JsonProcessor.cpp @@ -0,0 +1,457 @@ +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +#include "pch.h" + +/// +/// Loads a JSON file, parses its contents, and processes its configuration settings. +/// +/// The path to the JSON file. +/// The LoggerSettings object to populate with the parsed configuration data. +/// Returns true if the JSON file is successfully loaded and processed; otherwise, +/// returns false on error. +bool ReadConfigFile(_In_ const PWCHAR jsonFile, _Out_ LoggerSettings& Config) { + std::wstring wstr(jsonFile); + std::string jsonString = readJsonFromFile(wstr); + + boost::json::value parsedJson; + try { + parsedJson = boost::json::parse(jsonString); + const boost::json::value& logConfig = parsedJson.at("LogConfig"); + + if (!processLogConfig(logConfig, Config)) { + logWriter.TraceError(L"Failed to fully process LogConfig."); + return false; + } + } + catch (const boost::system::system_error& e) { + logWriter.TraceError( + Utility::FormatString( + L"Error parsing JSON: %S", + e.what() + ).c_str() + ); + return false; + } + catch (const std::exception& e) { + logWriter.TraceError( + Utility::FormatString( + L"An unexpected error occurred: %S", + e.what() + ).c_str() + ); + return false; + } + catch (...) { + logWriter.TraceError(L"An unknown error occurred."); + return false; + } + + if (parsedJson.is_null()) { + logWriter.TraceError(L"Parsed JSON is null."); + return false; + } + + return true; +} + +/// +/// Reads a JSON file from a given file path and returns its contents as a UTF-8 encoded string. +/// +/// The path to the JSON file. +/// A UTF-8 encoded string containing the JSON file's contents, +/// or an empty string if the file could not be opened. +std::string readJsonFromFile(_In_ const std::wstring& filePath) { + // Open the file as a wide character input stream + std::wifstream wif(filePath); + + if (!wif.is_open()) { + logWriter.TraceError(L"Failed to open JSON file."); + return ""; + } + + // Read the file into a wide string buffer + std::wstringstream wss; + wss << wif.rdbuf(); + wif.close(); + + // Convert the wstring buffer to a UTF-8 string + std::wstring_convert> converter; + std::string jsonString = converter.to_bytes(wss.str()); + + return jsonString; +} + +/// +/// Parses and processes configuration data for an EventLog log source, initializing a SourceEventLog object. +/// +/// JSON configuration data. +/// Map of attributes used to store the configuration details. +/// Vector of log sources. +/// +/// Returns true if the Event log source is successfully parsed and added to Sources; +/// otherwise, returns false if parsing fails. +/// +bool handleEventLog( + _In_ const boost::json::value& source, + _In_ AttributesMap& Attributes, + _Inout_ std::vector>& Sources +) { + try { + bool startAtOldest = source.as_object().at("startAtOldestRecord").as_bool(); + bool multiLine = source.at("eventFormatMultiLine").as_bool(); + std::string customLogFormat = source.as_object().at("customLogFormat").as_string().c_str(); + + Attributes[JSON_TAG_START_AT_OLDEST_RECORD] = reinterpret_cast( + std::make_unique(startAtOldest ? L"true" : L"false").release() + ); + + Attributes[JSON_TAG_FORMAT_MULTILINE] = reinterpret_cast( + std::make_unique(multiLine ? L"true" : L"false").release() + ); + + Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( + std::make_unique(Utility::string_to_wstring(customLogFormat)).release() + ); + + // Process channels if they exist + if (source.as_object().contains("channels")) { + auto channels = new std::vector(); + for (const auto& channel : source.as_object().at("channels").as_array()) { + std::string name = getJsonStringCaseInsensitive(channel.as_object(), "name"); + std::string levelString = getJsonStringCaseInsensitive(channel.as_object(), "level"); + + EventLogChannel eventChannel(Utility::string_to_wstring(name), EventChannelLogLevel::Error); + + std::wstring level = Utility::string_to_wstring(levelString); + // Set the level based on the levelString, logging an error if invalid + if (!eventChannel.SetLevelByString(level)) { + logWriter.TraceError( + Utility::FormatString( + L"Invalid level string: %S", + level.c_str() + ).c_str() + ); + return false; + } + + channels->push_back(eventChannel); // Add to the vector + } + + Attributes[JSON_TAG_CHANNELS] = reinterpret_cast(channels); + } + else { + Attributes[JSON_TAG_CHANNELS] = nullptr; + } + + auto sourceEventLog = std::make_shared(); + if (!SourceEventLog::Unwrap(Attributes, *sourceEventLog)) { + logWriter.TraceError(L"Error parsing configuration file. Invalid EventLog source"); + return false; + } + + Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceEventLog))); + return true; + } + catch (const std::exception& e) { + logWriter.TraceError( + Utility::FormatString( + L"Error parsing EventLog source: %S", + e.what() + ).c_str() + ); + return false; + } +} + +/// +/// Parses and processes configuration data specific to File type logs, initializing a SourceFile object. +/// +/// JSON value containing configuration data. +/// Map of attributes for storing configuration details. +/// Vector of log sources. +/// +/// Returns true if the File log source is successfully parsed and added to Sources; +/// otherwise, returns false if parsing fails. +/// +bool handleFileLog( + _In_ const boost::json::value& source, + _In_ AttributesMap& Attributes, + _Inout_ std::vector>& Sources +) { + std::string directory = getJsonStringCaseInsensitive(source.as_object(), "directory"); + std::string filter = getJsonStringCaseInsensitive(source.as_object(), "filter"); + bool includeSubdirs = source.at("includeSubdirectories").as_bool(); + std::string customLogFormat = getJsonStringCaseInsensitive(source.as_object(), "customLogFormat"); + + Attributes[JSON_TAG_DIRECTORY] = reinterpret_cast( + std::make_unique(Utility::string_to_wstring(directory)).release() + ); + Attributes[JSON_TAG_FILTER] = reinterpret_cast( + std::make_unique(Utility::string_to_wstring(filter)).release() + ); + Attributes[JSON_TAG_INCLUDE_SUBDIRECTORIES] = reinterpret_cast( + std::make_unique(includeSubdirs ? L"true" : L"false").release() + ); + Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( + std::make_unique(Utility::string_to_wstring(customLogFormat)).release() + ); + + auto sourceFile = std::make_shared(); + if (!SourceFile::Unwrap(Attributes, *sourceFile)) { + logWriter.TraceError(L"Error parsing configuration file. Invalid File source"); + return false; + } + + Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceFile))); + + return true; +} + +/// +/// Parses and processes configuration details specific to ETW logs, initializing a SourceETW object. +/// +/// JSON value containing configuration data. +/// Map of attributes for storing configuration details. +/// Vector of log sources. +/// +/// Returns true if the ETW log source is successfully parsed and added to Sources; +/// otherwise, returns false if parsing fails. +/// +bool handleETWLog( + _In_ const boost::json::value& source, + _In_ AttributesMap& Attributes, + _Inout_ std::vector>& Sources +) { + bool multiLine = source.at("eventFormatMultiLine").as_bool(); + std::string customLogFormat = source.at("customLogFormat").as_string().c_str(); + + // Store multiLine and customLogFormat as wide strings in Attributes. + Attributes[JSON_TAG_FORMAT_MULTILINE] = reinterpret_cast( + std::make_unique(multiLine ? L"true" : L"false").release() + ); + Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( + std::make_unique(Utility::string_to_wstring(customLogFormat)).release() + ); + + std::vector etwProviders; + + const boost::json::array& providers = source.at("providers").as_array(); + for (const auto& provider : providers) { + std::string providerName = getJsonStringCaseInsensitive(provider.as_object(), "providerName"); + std::string providerGuid = getJsonStringCaseInsensitive(provider.as_object(), "providerGuid"); + std::string level = getJsonStringCaseInsensitive(provider.as_object(), "level"); + + ETWProvider etwProvider; + etwProvider.ProviderName = Utility::string_to_wstring(providerName); + etwProvider.SetProviderGuid(Utility::string_to_wstring(providerGuid)); + etwProvider.StringToLevel(Utility::string_to_wstring(level)); + + // Check if "keywords" exists and process it if available. + auto keywordsIter = provider.as_object().find("keywords"); + if (keywordsIter != provider.as_object().end()) { + std::string keywords = keywordsIter->value().as_string().c_str(); + etwProvider.Keywords = wcstoull(Utility::string_to_wstring(keywords).c_str(), NULL, 0); + } + + etwProviders.push_back(etwProvider); + } + + // Store the ETW providers in Attributes. + Attributes[JSON_TAG_PROVIDERS] = reinterpret_cast( + std::make_unique>(std::move(etwProviders)).release() + ); + + auto sourceETW = std::make_shared(); + if (!SourceETW::Unwrap(Attributes, *sourceETW)) { + logWriter.TraceError( + L"Error parsing configuration file. " + L"Invalid ETW source (it must have a non-empty 'channels')" + ); + return false; + } + + Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceETW))); + + return true; +} + +/// +/// Parses and processes configuration details specific to Process logs, initializing a SourceProcess object. +/// +/// JSON value with configuration data. +/// Map of attributes for storing configuration details. +/// Vector of log sources. +/// +/// Returns true if the process log source is successfully parsed and added to Sources; +/// otherwise, returns false if parsing fails. +/// +bool handleProcessLog( + _In_ const boost::json::value& source, + _In_ AttributesMap& Attributes, + _Inout_ std::vector>& Sources +) { + std::string customLogFormat = source.at("customLogFormat").as_string().c_str(); + + Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( + std::make_unique(Utility::string_to_wstring(customLogFormat)).release() + ); + + auto sourceProcess = std::make_shared(); + if (!SourceProcess::Unwrap(Attributes, *sourceProcess)) { + logWriter.TraceError(L"Error parsing configuration file. Invalid Process source"); + return false; + } + + Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceProcess))); + + return true; +} + +/// +/// Processes the logging configuration from a JSON object, populating the LoggerSettings structure. +/// +/// JSON value containing logging configuration details. +/// LoggerSettings structure to populate with the parsed configuration. +/// +/// Returns true if the log configuration is valid and sources are successfully processed; +/// otherwise, returns false if the configuration is invalid or sources fail to process. +/// +bool processLogConfig(const boost::json::value& logConfig, _Out_ LoggerSettings& Config) { + if (!logConfig.is_object()) { + logWriter.TraceError(L"Invalid LogConfig object."); + return false; + } + + const boost::json::object& obj = logConfig.as_object(); + + std::string logFormat = getJsonStringCaseInsensitive(obj, "logFormat"); + if (!logFormat.empty()) { + Config.LogFormat = Utility::string_to_wstring(logFormat); + } else { + logWriter.TraceError(L"LogFormat not found in LogConfig. Using default log format."); + } + + if (!obj.contains("sources") || !obj.at("sources").is_array()) { + logWriter.TraceError(L"Sources array not found or invalid in LogConfig."); + return false; + } + + // Process the sources array + const boost::json::array& sources = obj.at("sources").as_array(); + return processSources(sources, Config); +} + +/// +/// Iterates through the sources array from the configuration, +/// parsing and processing each log source based on its type. +/// +/// JSON array containing different log sources. +/// LoggerSettings structure where parsed sources are stored. +/// +/// Returns true if all sources are successfully processed; +/// otherwise, returns false if any source fails to process. +/// +bool processSources(const boost::json::array& sources, _Out_ LoggerSettings& Config) { + bool overallSuccess = true; + + for (const auto& source : sources) { + if (!source.is_object()) { + logWriter.TraceError(L"Skipping invalid source entry (not an object)."); + overallSuccess = false; + continue; + } + + const boost::json::object& srcObj = source.as_object(); + std::string sourceType = getJsonStringCaseInsensitive(srcObj, "type"); + + if (sourceType.empty()) { + logWriter.TraceError(L"Skipping source with missing or empty type."); + overallSuccess = false; + continue; + } + + AttributesMap sourceAttributes; + bool parseSuccess = false; + + if (sourceType == "EventLog") { + parseSuccess = handleEventLog(source, sourceAttributes, Config.Sources); + } else if (sourceType == "File") { + parseSuccess = handleFileLog(source, sourceAttributes, Config.Sources); + } else if (sourceType == "ETW") { + parseSuccess = handleETWLog(source, sourceAttributes, Config.Sources); + } else if (sourceType == "Process") { + parseSuccess = handleProcessLog(source, sourceAttributes, Config.Sources); + } else { + logWriter.TraceError( + Utility::FormatString( + L"Invalid source type: %S", + Utility::string_to_wstring(sourceType).c_str() + ).c_str() + ); + overallSuccess = false; + continue; + } + + if (!parseSuccess) { + logWriter.TraceError( + Utility::FormatString( + L"Failed to process source of type: %S", + Utility::string_to_wstring(sourceType).c_str() + ).c_str() + ); + overallSuccess = false; + } + + cleanupAttributes(sourceAttributes); + } + + return overallSuccess; +} + +/// +/// Cleans up dynamically allocated memory in the Attributes map by deleting each non-null attribute pointer. +/// +/// A map of attribute keys to pointers. +void cleanupAttributes(_In_ AttributesMap& Attributes) { + for (auto attributePair : Attributes) + { + if (attributePair.second != nullptr) + { + delete attributePair.second; + } + } +} + +/// +/// Retrieves a string value from a JSON object in a case-insensitive manner. +/// +/// The JSON object to search for the key. +/// The key to search for in the JSON object, case-insensitive. +/// string value associated with the key if found +std::string getJsonStringCaseInsensitive(_In_ const boost::json::object& obj, _In_ const std::string& key) { + auto it = std::find_if(obj.begin(), obj.end(), + [&](const auto& item) { + std::string currentKey = std::string(item.key().data()); + std::transform(currentKey.begin(), currentKey.end(), currentKey.begin(), ::tolower); + std::string lowerKey = key; + std::transform(lowerKey.begin(), lowerKey.end(), lowerKey.begin(), ::tolower); + return currentKey == lowerKey; + }); + + if (it != obj.end() && it->value().is_string()) { + return std::string(it->value().as_string().data()); + } + + logWriter.TraceError( + Utility::FormatString( + L"Key %S not found in JSON object", key.c_str() + ).c_str() + ); + + // Return an empty string if the key is not found or the value is not a string. + return ""; +} + diff --git a/LogMonitor/src/LogMonitor/JsonProcessor.h b/LogMonitor/src/LogMonitor/JsonProcessor.h new file mode 100644 index 00000000..fd778edd --- /dev/null +++ b/LogMonitor/src/LogMonitor/JsonProcessor.h @@ -0,0 +1,58 @@ +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +#pragma once + +bool handleEventLog( + _In_ const boost::json::value& source, + _In_ AttributesMap& Attributes, + _Inout_ std::vector>& Sources +); + +bool handleFileLog( + _In_ const boost::json::value& source, + _In_ AttributesMap& Attributes, + _Inout_ std::vector>& Sources +); + +bool handleETWLog( + _In_ const boost::json::value& source, + _In_ AttributesMap& Attributes, + _Inout_ std::vector>& Sources +); + +bool handleProcessLog( + _In_ const boost::json::value& source, + _In_ AttributesMap& Attributes, + _Inout_ std::vector>& Sources +); + +bool ReadConfigFile( + _In_ const PWCHAR jsonFile, + _Out_ LoggerSettings& Config +); + +std::string readJsonFromFile( + _In_ const std::wstring& filePath +); + +bool processLogConfig( + const boost::json::value& logConfig, + _Out_ LoggerSettings& Config +); + +bool processSources( + const boost::json::array& sources, + _Out_ LoggerSettings& Config +); + +void cleanupAttributes( + _In_ AttributesMap& Attributes +); + +std::string getJsonStringCaseInsensitive( + _In_ const boost::json::object& obj, + _In_ const std::string& key +); diff --git a/LogMonitor/src/LogMonitor/LogMonitor.vcxproj b/LogMonitor/src/LogMonitor/LogMonitor.vcxproj index 92b3de94..5976a443 100644 --- a/LogMonitor/src/LogMonitor/LogMonitor.vcxproj +++ b/LogMonitor/src/LogMonitor/LogMonitor.vcxproj @@ -160,10 +160,9 @@ + - - @@ -172,11 +171,10 @@ - - + diff --git a/LogMonitor/src/LogMonitor/LogMonitor.vcxproj.filters b/LogMonitor/src/LogMonitor/LogMonitor.vcxproj.filters index 56eecf60..6a030137 100644 --- a/LogMonitor/src/LogMonitor/LogMonitor.vcxproj.filters +++ b/LogMonitor/src/LogMonitor/LogMonitor.vcxproj.filters @@ -36,12 +36,6 @@ Header Files - - Header Files - - - Header Files - Header Files @@ -54,14 +48,14 @@ Header Files + + Header Files + Source Files - - Source Files - Source Files @@ -80,10 +74,10 @@ Source Files - + Source Files - + Source Files diff --git a/LogMonitor/src/LogMonitor/Main.cpp b/LogMonitor/src/LogMonitor/Main.cpp index 436da509..1b35766f 100644 --- a/LogMonitor/src/LogMonitor/Main.cpp +++ b/LogMonitor/src/LogMonitor/Main.cpp @@ -381,7 +381,7 @@ int __cdecl wmain(int argc, WCHAR *argv[]) LoggerSettings settings; //read the config file - bool configFileReadSuccess = OpenConfigFile(configFileName, settings); + bool configFileReadSuccess = ReadConfigFile(configFileName, settings); //start the monitors if (configFileReadSuccess) diff --git a/LogMonitor/src/LogMonitor/Parser/LoggerSettings.h b/LogMonitor/src/LogMonitor/Parser/LoggerSettings.h index 2817da1a..45cb61e3 100644 --- a/LogMonitor/src/LogMonitor/Parser/LoggerSettings.h +++ b/LogMonitor/src/LogMonitor/Parser/LoggerSettings.h @@ -170,6 +170,12 @@ typedef struct _EventLogChannel std::wstring Name; EventChannelLogLevel Level = EventChannelLogLevel::Error; + _EventLogChannel() + : Name(L""), Level(EventChannelLogLevel::Error) {} + + _EventLogChannel(const std::wstring& name, EventChannelLogLevel level = EventChannelLogLevel::Error) + : Name(name), Level(level) {} + inline bool IsValid() { return !Name.empty(); diff --git a/LogMonitor/src/LogMonitor/Utility.cpp b/LogMonitor/src/LogMonitor/Utility.cpp index d9203e13..69b86dea 100644 --- a/LogMonitor/src/LogMonitor/Utility.cpp +++ b/LogMonitor/src/LogMonitor/Utility.cpp @@ -438,3 +438,22 @@ bool Utility::IsCustomJsonFormat(_Inout_ std::wstring& customLogFormat) return isCustomJSONFormat; } +/// +/// Function to convert wstring to string (UTF-8) +/// +/// +/// +std::string Utility::wstring_to_string(_In_ const std::wstring& wstr) { + std::wstring_convert> converter; + return converter.to_bytes(wstr); +} + +/// +/// Function to convert string to wstring (UTF-8) +/// +/// The input string to be converted +/// A wide string representation of the input string +std::wstring Utility::string_to_wstring(_In_ const std::string& str) { + std::wstring_convert> converter; + return converter.from_bytes(str); +} diff --git a/LogMonitor/src/LogMonitor/Utility.h b/LogMonitor/src/LogMonitor/Utility.h index 11d0d36f..37155671 100644 --- a/LogMonitor/src/LogMonitor/Utility.h +++ b/LogMonitor/src/LogMonitor/Utility.h @@ -90,4 +90,8 @@ class Utility final ); static bool IsCustomJsonFormat(_Inout_ std::wstring& customLogFormat); + + static std::string wstring_to_string(_In_ const std::wstring& wstr); + + static std::wstring string_to_wstring(_In_ const std::string& str); }; diff --git a/LogMonitor/src/LogMonitor/pch.h b/LogMonitor/src/LogMonitor/pch.h index 92483da6..9612d3c3 100644 --- a/LogMonitor/src/LogMonitor/pch.h +++ b/LogMonitor/src/LogMonitor/pch.h @@ -47,6 +47,7 @@ #include "shlwapi.h" #include #include +#include #include "Utility.h" #include "Parser/ConfigFileParser.h" #include "Parser/LoggerSettings.h" @@ -57,5 +58,6 @@ #include "FileMonitor/FileMonitorUtilities.h" #include "LogFileMonitor.h" #include "ProcessMonitor.h" +#include "JsonProcessor.h" #endif //PCH_H diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c0c96ef4..5d981bc5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,84 +2,149 @@ trigger: - main pool: - vmImage: 'windows-2022' # name of the pool to run this job in - demands: - - msbuild - - visualstudio - - vstest + vmImage: 'windows-2022' + demands: + - msbuild + - visualstudio + - vstest variables: solution: '**/*.sln' - buildPlatform: 'x86|x64|ARM' + buildPlatform: 'x64' # Set to x64 or ARM64 depending on the build you want buildConfiguration: 'Release' + VCPKG_ROOT: '$(Build.SourcesDirectory)\vcpkg' + VCPKG_CMAKE_OPTIONS: '-DCMAKE_TOOLCHAIN_FILE=$(VCPKG_ROOT)\scripts\buildsystems\vcpkg.cmake' + SDKVersion: '' jobs: - - job: x64_build + - job: build steps: - - task: VSBuild@1 - inputs: - platform: 'x64' - solution: '$(solution)' - configuration: '$(buildConfiguration)' + - task: PowerShell@2 + displayName: 'Install vcpkg' + inputs: + targetType: 'inline' + script: | + echo "Cloning and bootstrapping vcpkg" + git clone https://github.com/microsoft/vcpkg.git $(VCPKG_ROOT) + cd $(VCPKG_ROOT) + .\bootstrap-vcpkg.bat + .\vcpkg.exe integrate install # Integrate vcpkg with MSBuild - - task: ComponentGovernanceComponentDetection@0 - inputs: - scanType: 'Register' - verbosity: 'Verbose' - alertWarningLevel: 'Low' + # Install Boost JSON for the selected platform (x64 or ARM64) + - task: PowerShell@2 + displayName: 'Install Boost JSON' + inputs: + targetType: 'inline' + script: | + if (!(Test-Path "$(VCPKG_ROOT)\vcpkg.exe")) { + Write-Error "vcpkg.exe not found. Exiting..." + exit 1 + } + Write-Host "Installing Boost JSON for platform: $(buildPlatform)..." + if ('$(buildPlatform)' -eq 'x64') { + & "$(VCPKG_ROOT)\vcpkg.exe" install boost-json:x64-windows + } elseif ('$(buildPlatform)' -eq 'ARM64') { + & "$(VCPKG_ROOT)\vcpkg.exe" install boost-json:arm64-windows + } else { + Write-Error "Unsupported build platform: $(buildPlatform)" + exit 1 + } - - task: VisualStudioTestPlatformInstaller@1 - inputs: - packageFeedSelector: 'nugetOrg' - versionSelector: 'latestPreRelease' + # Verify vcpkg integration + - task: PowerShell@2 + displayName: 'Verify vcpkg Integration' + inputs: + targetType: 'inline' + script: | + echo "Verifying vcpkg integration for platform: $(buildPlatform)..." + & "$(VCPKG_ROOT)\vcpkg.exe" integrate install + if ('$(buildPlatform)' -eq 'x64') { + if (!(Test-Path "$(VCPKG_ROOT)\installed\x64-windows\include\boost\json.hpp")) { + Write-Error "Boost JSON header not found for x64. Exiting..." + exit 1 + } + } elseif ('$(buildPlatform)' -eq 'ARM64') { + if (!(Test-Path "$(VCPKG_ROOT)\installed\arm64-windows\include\boost\json.hpp")) { + Write-Error "Boost JSON header not found for ARM64. Exiting..." + exit 1 + } + } + Write-Output "vcpkg integration verified." - - task: VSTest@2 - inputs: - testSelector: 'testAssemblies' - testAssemblyVer2: '**\*test*.dll' - searchFolder: '$(System.DefaultWorkingDirectory)' - runOnlyImpactedTests: false - runInParallel: false - rerunFailedTests: true - rerunMaxAttempts: 3 - - - task: PublishPipelineArtifact@1 - inputs: - targetPath: '$(Build.SourcesDirectory)\LogMonitor\x64\Release\' - artifactType: 'pipeline' - artifactName: '64-bit' - - - job: x86_build - steps: - - task: VSBuild@1 + # Get installed Windows SDK version + - task: PowerShell@2 + displayName: 'Check Installed Windows SDK Version' inputs: - platform: 'x86' - solution: '$(solution)' - configuration: '$(buildConfiguration)' + targetType: 'inline' + script: | + $x86SdkPath = "C:\Program Files (x86)\Windows Kits\10\Include" + $x64SdkPath = "C:\Program Files\Windows Kits\10\Include" + $armSdkPath = "C:\Program Files\Windows Kits\10\Include\arm64" + + function Get-SdkVersion { + param ($sdkPath) + $sdkVersion = Get-ChildItem $sdkPath | Where-Object { $_.PSIsContainer -and $_.Name -match '^\d+\.\d+' } | Sort-Object -Property Name | Select-Object -Last 1 + return $sdkVersion + } + + $sdkFolder = $null + if (Test-Path $x64SdkPath) { + $sdkFolder = Get-SdkVersion -sdkPath $x64SdkPath + } elseif (Test-Path $x86SdkPath) { + $sdkFolder = Get-SdkVersion -sdkPath $x86SdkPath + } elseif (Test-Path $armSdkPath) { + $sdkFolder = Get-SdkVersion -sdkPath $armSdkPath + } + + if ($sdkFolder) { + Write-Host "Installed Windows SDK version: $($sdkFolder.Name)" + Write-Host "##vso[task.setvariable variable=SDKVersion]$($sdkFolder.Name)" + } else { + Write-Host "Windows SDK not found!" + exit 1 + } + # Configure CMake based on selected platform + - task: CMake@1 + displayName: 'Configure CMake' + inputs: + workingDirectory: '$(Build.SourcesDirectory)\LogMonitor' + cmakeArgs: | + -A $(buildPlatform) -S $(Build.SourcesDirectory)\LogMonitor -B $(Build.BinariesDirectory)\LogMonitor\$(buildPlatform) + + - task: CMake@1 + displayName: 'Build with CMake' + inputs: + workingDirectory: '$(Build.BinariesDirectory)\LogMonitor\$(buildPlatform)' + cmakeArgs: '--build . --config $(buildConfiguration) --parallel' + + # Component Governance - task: ComponentGovernanceComponentDetection@0 inputs: - scanType: 'LogOnly' + scanType: 'Register' verbosity: 'Verbose' - alertWarningLevel: 'High' + alertWarningLevel: 'Low' + # Install Visual Studio Test Platform - task: VisualStudioTestPlatformInstaller@1 inputs: packageFeedSelector: 'nugetOrg' versionSelector: 'latestPreRelease' + # Run Tests - task: VSTest@2 inputs: testSelector: 'testAssemblies' testAssemblyVer2: '**\*test*.dll' - searchFolder: '$(System.DefaultWorkingDirectory)' + searchFolder: '$(Build.BinariesDirectory)\LogMonitor\$(buildPlatform)\$(buildConfiguration)\' runOnlyImpactedTests: false runInParallel: false rerunFailedTests: true rerunMaxAttempts: 3 - + + # Publish build artifacts - task: PublishPipelineArtifact@1 inputs: - targetPath: '$(Build.SourcesDirectory)\LogMonitor\Release\' + targetPath: '$(Build.BinariesDirectory)\LogMonitor\$(buildPlatform)\$(buildConfiguration)\' artifactType: 'pipeline' - artifactName: '32-bit' + artifactName: '$(buildPlatform)-$(buildConfiguration)' From 1934708e1bd90b6eb2e61c3d38cf73f5fe305438 Mon Sep 17 00:00:00 2001 From: Charity Kathure Date: Wed, 8 Apr 2026 21:07:21 +0300 Subject: [PATCH 2/4] Nlohmann json Implementation (#211) Signed-off-by: Charity Kathure * review: fix memory safety, error handling, and build improvements - Use unique_ptr for channels vector in handleEventLog to eliminate manual delete and prevent memory leak on early return paths - SanitizeJson no longer replaces log content with placeholder on failure; leaves str unchanged and logs via logWriter instead - getJsonStringCaseInsensitive adds required param (default false) so optional fields don't emit spurious errors; required fields (name, level, directory, providerName, providerGuid) now pass true - Fix inconsistent nlohmann::json vs json alias in handleETWLog/handleProcessLog - build.cmd: detect cmake from VS or vcpkg when not on PATH; use goto to avoid delayed expansion issues with nested if blocks - docs: remove leading space from "Information" level in README example --------- Signed-off-by: Charity Kathure Co-authored-by: Charity Kathure Co-authored-by: Bob Sira --- LogMonitor/CMakeLists.txt | 37 +++- LogMonitor/LogMonitorTests/CMakeLists.txt | 8 +- LogMonitor/LogMonitorTests/UtilityTests.cpp | 3 + LogMonitor/LogMonitorTests/pch.h | 1 + LogMonitor/docs/README.md | 2 +- LogMonitor/src/CMakeLists.txt | 39 +++- LogMonitor/src/LogMonitor/JsonProcessor.cpp | 230 ++++++++++++-------- LogMonitor/src/LogMonitor/JsonProcessor.h | 19 +- LogMonitor/src/LogMonitor/Utility.cpp | 68 +++--- LogMonitor/src/LogMonitor/pch.h | 2 +- azure-pipelines.yml | 48 +--- build.cmd | 81 ++++++- 12 files changed, 330 insertions(+), 208 deletions(-) diff --git a/LogMonitor/CMakeLists.txt b/LogMonitor/CMakeLists.txt index dfbbd4b8..b44b0d51 100644 --- a/LogMonitor/CMakeLists.txt +++ b/LogMonitor/CMakeLists.txt @@ -3,21 +3,30 @@ cmake_minimum_required(VERSION 3.15) # Define the project project(LogMonitor) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +set(Boost_USE_STATIC_LIBS ON) +set(Boost_USE_STATIC_RUNTIME ON) + # Set C++ standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_BUILD_TYPE Release) -set(VCPKG_TARGET_TRIPLET x64-windows) +set(VCPKG_TARGET_TRIPLET x64-windows-static) # Use vcpkg if available -if (DEFINED ENV{VCPKG_ROOT}) +if(DEFINED ENV{VCPKG_ROOT}) set(VCPKG_ROOT $ENV{VCPKG_ROOT}) - set(CMAKE_TOOLCHAIN_FILE "${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Toolchain file for vcpkg" FORCE) - - # Set Boost paths - set(BOOST_ROOT "${VCPKG_ROOT}/installed/x64-windows" CACHE PATH "Boost installation root") - set(Boost_INCLUDE_DIR "${VCPKG_ROOT}/installed/x64-windows/include" CACHE PATH "Boost include directory") + set(CMAKE_TOOLCHAIN_FILE "${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file" FORCE) + set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "Vcpkg target triplet") +endif() + +# Enforce static MSVC runtime (/MT or /MTd) +if(MSVC) + foreach(flag_var CMAKE_C_FLAGS_RELEASE CMAKE_C_FLAGS_DEBUG + CMAKE_CXX_FLAGS_RELEASE CMAKE_CXX_FLAGS_DEBUG) + string(REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + endforeach() endif() # Set Windows SDK version if available @@ -25,11 +34,21 @@ if (DEFINED ENV{SDKVersion}) set(CMAKE_SYSTEM_VERSION $ENV{SDKVersion}) endif() +# Enable Unicode globally +add_definitions(-DUNICODE -D_UNICODE) + +# Enable warnings +if (MSVC) + add_compile_options(/W4) +else() + add_compile_options(-Wall -Wextra -pedantic) +endif() + # Enable testing framework enable_testing() -# Enable Unicode globally -add_definitions(-DUNICODE -D_UNICODE) +# Find dependencies +find_package(nlohmann_json CONFIG REQUIRED) # Include subdirectories for main and test executables add_subdirectory(src) # Add main executable's CMake diff --git a/LogMonitor/LogMonitorTests/CMakeLists.txt b/LogMonitor/LogMonitorTests/CMakeLists.txt index 79c50abd..1b7a63be 100644 --- a/LogMonitor/LogMonitorTests/CMakeLists.txt +++ b/LogMonitor/LogMonitorTests/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.15) project(LogMonitorTests) -find_package(Boost REQUIRED COMPONENTS json) +find_package(nlohmann_json CONFIG REQUIRED) # Automatically gather all test source files file(GLOB_RECURSE TEST_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*.cpp") @@ -22,7 +22,7 @@ target_compile_definitions(LogMonitorTests PRIVATE LOGMONITORTESTS_EXPORTS) target_include_directories(LogMonitorTests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} # Includes Utility.h and pch.h ${CMAKE_CURRENT_SOURCE_DIR}/../src - ${Boost_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/../src/LogMonitor ) # Set Windows-specific linker flags @@ -33,8 +33,8 @@ set_target_properties(LogMonitorTests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" ) -# Link LogMonitor and Boost.JSON -target_link_libraries(LogMonitorTests PRIVATE LogMonitor Boost::json) +# Link LogMonitor and Nlohmann JSON +target_link_libraries(LogMonitorTests PRIVATE LogMonitorLib nlohmann_json::nlohmann_json) # Enable testing enable_testing() diff --git a/LogMonitor/LogMonitorTests/UtilityTests.cpp b/LogMonitor/LogMonitorTests/UtilityTests.cpp index beb9efee..8642d77d 100644 --- a/LogMonitor/LogMonitorTests/UtilityTests.cpp +++ b/LogMonitor/LogMonitorTests/UtilityTests.cpp @@ -60,6 +60,7 @@ namespace UtilityTests std::wstring expect = L"say, \\\"hello\\\""; Utility::SanitizeJson(str); Assert::IsTrue(str == expect, L"should escape \""); + str = L"\"hello\""; expect = L"\\\"hello\\\""; Utility::SanitizeJson(str); @@ -69,6 +70,7 @@ namespace UtilityTests expect = L"hello\\r\\nworld"; Utility::SanitizeJson(str); Assert::IsTrue(str == expect, L"should escape \r and \n"); + str = L"\r\nHello\r\n"; expect = L"\\r\\nHello\\r\\n"; Utility::SanitizeJson(str); @@ -78,6 +80,7 @@ namespace UtilityTests expect = L"\\\\Driver\\\\XX\\\\"; Utility::SanitizeJson(str); Assert::IsTrue(str == expect, L"should escape \\"); + str = L"C:\\Drive\\XX"; expect = L"C:\\\\Drive\\\\XX"; Utility::SanitizeJson(str); diff --git a/LogMonitor/LogMonitorTests/pch.h b/LogMonitor/LogMonitorTests/pch.h index c6ad8c39..6c2a6957 100644 --- a/LogMonitor/LogMonitorTests/pch.h +++ b/LogMonitor/LogMonitorTests/pch.h @@ -52,6 +52,7 @@ #include #include #include +#include #include "../src/LogMonitor/Utility.h" #include "../src/LogMonitor/Parser/ConfigFileParser.h" #include "../src/LogMonitor/Parser/LoggerSettings.h" diff --git a/LogMonitor/docs/README.md b/LogMonitor/docs/README.md index d7cfccf2..211fed59 100644 --- a/LogMonitor/docs/README.md +++ b/LogMonitor/docs/README.md @@ -192,7 +192,7 @@ Example 1 (Application channel, verboseness: Error): "channels": [ { "name": "system", - "level": " Information" + "level": "Information" } ] } diff --git a/LogMonitor/src/CMakeLists.txt b/LogMonitor/src/CMakeLists.txt index 8f570a9d..c8c9e60d 100644 --- a/LogMonitor/src/CMakeLists.txt +++ b/LogMonitor/src/CMakeLists.txt @@ -2,22 +2,45 @@ cmake_minimum_required(VERSION 3.15) project(LogMonitor) -find_package(Boost REQUIRED COMPONENTS json) +find_package(nlohmann_json CONFIG REQUIRED) +# Gather source files file(GLOB_RECURSE SourceFiles RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*.cpp") +file(GLOB_RECURSE HeaderFiles RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*.h") -# Define LogMonitor as a library -add_library(LogMonitor ${SourceFiles}) +# Define LogMonitorLib as a static library +add_library(LogMonitorLib STATIC ${SourceFiles}) + +# Set the output name of the static library to "LogMonitor.lib" +set_target_properties(LogMonitorLib PROPERTIES + OUTPUT_NAME "LogMonitor" +) + +# Define LogMonitor as an executable +add_executable(LogMonitor ${SourceFiles}) # Add precompiled headers (PCH) target_precompile_headers(LogMonitor PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor/pch.h) -# Include directories for LogMonitor +# Include directories for both targets +target_include_directories(LogMonitorLib PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor + ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor/FileMonitor +) + target_include_directories(LogMonitor PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor - ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor/FileMonitor - ${Boost_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/LogMonitor/FileMonitor ) -# Link Boost JSON to LogMonitor -target_link_libraries(LogMonitor PRIVATE Boost::json) +# Link dependencies +target_link_libraries(LogMonitorLib PRIVATE nlohmann_json::nlohmann_json) +target_link_libraries(LogMonitor PRIVATE LogMonitorLib nlohmann_json::nlohmann_json) + +# Set output path +set_target_properties(LogMonitor PROPERTIES + OUTPUT_NAME "LogMonitor" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" +) diff --git a/LogMonitor/src/LogMonitor/JsonProcessor.cpp b/LogMonitor/src/LogMonitor/JsonProcessor.cpp index b962b669..a0cd16e0 100644 --- a/LogMonitor/src/LogMonitor/JsonProcessor.cpp +++ b/LogMonitor/src/LogMonitor/JsonProcessor.cpp @@ -5,6 +5,13 @@ #include "pch.h" +#ifdef _WIN32 +#include +#define strcasecmp _stricmp +#endif + +using json = nlohmann::json; + /// /// Loads a JSON file, parses its contents, and processes its configuration settings. /// @@ -16,17 +23,22 @@ bool ReadConfigFile(_In_ const PWCHAR jsonFile, _Out_ LoggerSettings& Config) { std::wstring wstr(jsonFile); std::string jsonString = readJsonFromFile(wstr); - boost::json::value parsedJson; try { - parsedJson = boost::json::parse(jsonString); - const boost::json::value& logConfig = parsedJson.at("LogConfig"); + json parsedJson = json::parse(jsonString); + + if (!parsedJson.contains("LogConfig")) { + logWriter.TraceError(L"Missing 'LogConfig' section in JSON."); + return false; + } + + const auto& logConfig = parsedJson["LogConfig"]; if (!processLogConfig(logConfig, Config)) { logWriter.TraceError(L"Failed to fully process LogConfig."); return false; } } - catch (const boost::system::system_error& e) { + catch (const json::parse_error& e) { logWriter.TraceError( Utility::FormatString( L"Error parsing JSON: %S", @@ -49,11 +61,6 @@ bool ReadConfigFile(_In_ const PWCHAR jsonFile, _Out_ LoggerSettings& Config) { return false; } - if (parsedJson.is_null()) { - logWriter.TraceError(L"Parsed JSON is null."); - return false; - } - return true; } @@ -66,15 +73,14 @@ bool ReadConfigFile(_In_ const PWCHAR jsonFile, _Out_ LoggerSettings& Config) { std::string readJsonFromFile(_In_ const std::wstring& filePath) { // Open the file as a wide character input stream std::wifstream wif(filePath); - if (!wif.is_open()) { logWriter.TraceError(L"Failed to open JSON file."); return ""; - } + } // Read the file into a wide string buffer std::wstringstream wss; - wss << wif.rdbuf(); + wss << wif.rdbuf(); wif.close(); // Convert the wstring buffer to a UTF-8 string @@ -95,14 +101,25 @@ std::string readJsonFromFile(_In_ const std::wstring& filePath) { /// otherwise, returns false if parsing fails. /// bool handleEventLog( - _In_ const boost::json::value& source, + _In_ const json& source, _In_ AttributesMap& Attributes, _Inout_ std::vector>& Sources ) { try { - bool startAtOldest = source.as_object().at("startAtOldestRecord").as_bool(); - bool multiLine = source.at("eventFormatMultiLine").as_bool(); - std::string customLogFormat = source.as_object().at("customLogFormat").as_string().c_str(); + bool startAtOldest = false; + if (source.contains("startAtOldestRecord") && source["startAtOldestRecord"].is_boolean()) { + startAtOldest = source["startAtOldestRecord"].get(); + } + + bool multiLine = false; + if (source.contains("eventFormatMultiLine") && source["eventFormatMultiLine"].is_boolean()) { + multiLine = source["eventFormatMultiLine"].get(); + } + + std::string customLogFormat; + if (source.contains("customLogFormat") && source["customLogFormat"].is_string()) { + customLogFormat = source["customLogFormat"].get(); + } Attributes[JSON_TAG_START_AT_OLDEST_RECORD] = reinterpret_cast( std::make_unique(startAtOldest ? L"true" : L"false").release() @@ -117,11 +134,12 @@ bool handleEventLog( ); // Process channels if they exist - if (source.as_object().contains("channels")) { - auto channels = new std::vector(); - for (const auto& channel : source.as_object().at("channels").as_array()) { - std::string name = getJsonStringCaseInsensitive(channel.as_object(), "name"); - std::string levelString = getJsonStringCaseInsensitive(channel.as_object(), "level"); + if (source.contains("channels") && source["channels"].is_array()) { + auto channels = std::make_unique>(); + + for (const auto& channel : source["channels"]) { + std::string name = getJsonStringCaseInsensitive(channel, "name", true); + std::string levelString = getJsonStringCaseInsensitive(channel, "level", true); EventLogChannel eventChannel(Utility::string_to_wstring(name), EventChannelLogLevel::Error); @@ -131,16 +149,16 @@ bool handleEventLog( logWriter.TraceError( Utility::FormatString( L"Invalid level string: %S", - level.c_str() + levelString.c_str() ).c_str() ); return false; } - channels->push_back(eventChannel); // Add to the vector + channels->push_back(eventChannel); } - Attributes[JSON_TAG_CHANNELS] = reinterpret_cast(channels); + Attributes[JSON_TAG_CHANNELS] = reinterpret_cast(channels.release()); } else { Attributes[JSON_TAG_CHANNELS] = nullptr; @@ -154,16 +172,16 @@ bool handleEventLog( Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceEventLog))); return true; - } - catch (const std::exception& e) { - logWriter.TraceError( - Utility::FormatString( - L"Error parsing EventLog source: %S", - e.what() - ).c_str() - ); - return false; - } + } + catch (const std::exception& e) { + logWriter.TraceError( + Utility::FormatString( + L"Error parsing EventLog source: %S", + e.what() + ).c_str() + ); + return false; + } } /// @@ -177,14 +195,17 @@ bool handleEventLog( /// otherwise, returns false if parsing fails. /// bool handleFileLog( - _In_ const boost::json::value& source, + _In_ const json& source, _In_ AttributesMap& Attributes, _Inout_ std::vector>& Sources ) { - std::string directory = getJsonStringCaseInsensitive(source.as_object(), "directory"); - std::string filter = getJsonStringCaseInsensitive(source.as_object(), "filter"); - bool includeSubdirs = source.at("includeSubdirectories").as_bool(); - std::string customLogFormat = getJsonStringCaseInsensitive(source.as_object(), "customLogFormat"); + std::string directory = getJsonStringCaseInsensitive(source, "directory", true); + std::string filter = getJsonStringCaseInsensitive(source, "filter"); + bool includeSubdirs = false; + if (source.contains("includeSubdirectories") && source["includeSubdirectories"].is_boolean()) { + includeSubdirs = source["includeSubdirectories"].get(); + } + std::string customLogFormat = getJsonStringCaseInsensitive(source, "customLogFormat"); Attributes[JSON_TAG_DIRECTORY] = reinterpret_cast( std::make_unique(Utility::string_to_wstring(directory)).release() @@ -206,7 +227,6 @@ bool handleFileLog( } Sources.push_back(std::reinterpret_pointer_cast(std::move(sourceFile))); - return true; } @@ -221,14 +241,21 @@ bool handleFileLog( /// otherwise, returns false if parsing fails. /// bool handleETWLog( - _In_ const boost::json::value& source, + _In_ const json& source, _In_ AttributesMap& Attributes, _Inout_ std::vector>& Sources ) { - bool multiLine = source.at("eventFormatMultiLine").as_bool(); - std::string customLogFormat = source.at("customLogFormat").as_string().c_str(); + bool multiLine = false; + if (source.contains("eventFormatMultiLine") && source["eventFormatMultiLine"].is_boolean()) { + multiLine = source["eventFormatMultiLine"].get(); + } - // Store multiLine and customLogFormat as wide strings in Attributes. + std::string customLogFormat; + if (source.contains("customLogFormat") && source["customLogFormat"].is_string()) { + customLogFormat = source["customLogFormat"].get(); + } + + // Store multiLine and customLogFormat as wide strings in Attributes Attributes[JSON_TAG_FORMAT_MULTILINE] = reinterpret_cast( std::make_unique(multiLine ? L"true" : L"false").release() ); @@ -238,25 +265,25 @@ bool handleETWLog( std::vector etwProviders; - const boost::json::array& providers = source.at("providers").as_array(); - for (const auto& provider : providers) { - std::string providerName = getJsonStringCaseInsensitive(provider.as_object(), "providerName"); - std::string providerGuid = getJsonStringCaseInsensitive(provider.as_object(), "providerGuid"); - std::string level = getJsonStringCaseInsensitive(provider.as_object(), "level"); - - ETWProvider etwProvider; - etwProvider.ProviderName = Utility::string_to_wstring(providerName); - etwProvider.SetProviderGuid(Utility::string_to_wstring(providerGuid)); - etwProvider.StringToLevel(Utility::string_to_wstring(level)); - - // Check if "keywords" exists and process it if available. - auto keywordsIter = provider.as_object().find("keywords"); - if (keywordsIter != provider.as_object().end()) { - std::string keywords = keywordsIter->value().as_string().c_str(); - etwProvider.Keywords = wcstoull(Utility::string_to_wstring(keywords).c_str(), NULL, 0); - } + if (source.contains("providers") && source["providers"].is_array()) { + for (const auto& provider : source["providers"]) { + std::string providerName = getJsonStringCaseInsensitive(provider, "providerName", true); + std::string providerGuid = getJsonStringCaseInsensitive(provider, "providerGuid", true); + std::string level = getJsonStringCaseInsensitive(provider, "level", true); + + ETWProvider etwProvider; + etwProvider.ProviderName = Utility::string_to_wstring(providerName); + etwProvider.SetProviderGuid(Utility::string_to_wstring(providerGuid)); + etwProvider.StringToLevel(Utility::string_to_wstring(level)); + + // Check if "keywords" exists and process it + if (provider.contains("keywords") && provider["keywords"].is_string()) { + std::string keywords = provider["keywords"].get(); + etwProvider.Keywords = wcstoull(Utility::string_to_wstring(keywords).c_str(), nullptr, 0); + } - etwProviders.push_back(etwProvider); + etwProviders.push_back(std::move(etwProvider)); + } } // Store the ETW providers in Attributes. @@ -289,11 +316,14 @@ bool handleETWLog( /// otherwise, returns false if parsing fails. /// bool handleProcessLog( - _In_ const boost::json::value& source, + _In_ const json& source, _In_ AttributesMap& Attributes, _Inout_ std::vector>& Sources ) { - std::string customLogFormat = source.at("customLogFormat").as_string().c_str(); + std::string customLogFormat; + if (source.contains("customLogFormat") && source["customLogFormat"].is_string()) { + customLogFormat = source["customLogFormat"].get(); + } Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( std::make_unique(Utility::string_to_wstring(customLogFormat)).release() @@ -319,28 +349,32 @@ bool handleProcessLog( /// Returns true if the log configuration is valid and sources are successfully processed; /// otherwise, returns false if the configuration is invalid or sources fail to process. /// -bool processLogConfig(const boost::json::value& logConfig, _Out_ LoggerSettings& Config) { +bool processLogConfig(_In_ const nlohmann::json& logConfig, _Out_ LoggerSettings& Config) { if (!logConfig.is_object()) { logWriter.TraceError(L"Invalid LogConfig object."); return false; } - const boost::json::object& obj = logConfig.as_object(); + const auto& obj = logConfig; + + std::string logFormat; + if (obj.contains("logFormat") && obj["logFormat"].is_string()) { + logFormat = obj["logFormat"].get(); + } - std::string logFormat = getJsonStringCaseInsensitive(obj, "logFormat"); if (!logFormat.empty()) { Config.LogFormat = Utility::string_to_wstring(logFormat); - } else { + } else { logWriter.TraceError(L"LogFormat not found in LogConfig. Using default log format."); - } + } - if (!obj.contains("sources") || !obj.at("sources").is_array()) { + if (!obj.contains("sources") || !obj["sources"].is_array()) { logWriter.TraceError(L"Sources array not found or invalid in LogConfig."); return false; } // Process the sources array - const boost::json::array& sources = obj.at("sources").as_array(); + const auto& sources = obj["sources"]; return processSources(sources, Config); } @@ -354,9 +388,14 @@ bool processLogConfig(const boost::json::value& logConfig, _Out_ LoggerSettings& /// Returns true if all sources are successfully processed; /// otherwise, returns false if any source fails to process. /// -bool processSources(const boost::json::array& sources, _Out_ LoggerSettings& Config) { +bool processSources(_In_ const nlohmann::json& sources, _Out_ LoggerSettings& Config) { bool overallSuccess = true; + if (!sources.is_array()) { + logWriter.TraceError(L"Sources is not an array."); + return false; + } + for (const auto& source : sources) { if (!source.is_object()) { logWriter.TraceError(L"Skipping invalid source entry (not an object)."); @@ -364,8 +403,12 @@ bool processSources(const boost::json::array& sources, _Out_ LoggerSettings& Con continue; } - const boost::json::object& srcObj = source.as_object(); - std::string sourceType = getJsonStringCaseInsensitive(srcObj, "type"); + const auto& srcObj = source; + + std::string sourceType; + if (srcObj.contains("type") && srcObj["type"].is_string()) { + sourceType = srcObj["type"].get(); + } if (sourceType.empty()) { logWriter.TraceError(L"Skipping source with missing or empty type."); @@ -402,13 +445,13 @@ bool processSources(const boost::json::array& sources, _Out_ LoggerSettings& Con Utility::string_to_wstring(sourceType).c_str() ).c_str() ); - overallSuccess = false; + overallSuccess = false; } cleanupAttributes(sourceAttributes); } - return overallSuccess; + return overallSuccess; } /// @@ -431,27 +474,26 @@ void cleanupAttributes(_In_ AttributesMap& Attributes) { /// The JSON object to search for the key. /// The key to search for in the JSON object, case-insensitive. /// string value associated with the key if found -std::string getJsonStringCaseInsensitive(_In_ const boost::json::object& obj, _In_ const std::string& key) { - auto it = std::find_if(obj.begin(), obj.end(), - [&](const auto& item) { - std::string currentKey = std::string(item.key().data()); - std::transform(currentKey.begin(), currentKey.end(), currentKey.begin(), ::tolower); - std::string lowerKey = key; - std::transform(lowerKey.begin(), lowerKey.end(), lowerKey.begin(), ::tolower); - return currentKey == lowerKey; - }); - - if (it != obj.end() && it->value().is_string()) { - return std::string(it->value().as_string().data()); +std::string getJsonStringCaseInsensitive(_In_ const nlohmann::json& obj, _In_ const std::string& key, _In_ bool required) { + auto lowerKey = key; + std::transform(lowerKey.begin(), lowerKey.end(), lowerKey.begin(), ::tolower); + + for (auto it = obj.begin(); it != obj.end(); ++it) { + std::string currentKey = it.key(); + std::transform(currentKey.begin(), currentKey.end(), currentKey.begin(), ::tolower); + if (currentKey == lowerKey && it.value().is_string()) { + return it.value().get(); + } } - logWriter.TraceError( - Utility::FormatString( - L"Key %S not found in JSON object", key.c_str() - ).c_str() - ); + if (required) { + logWriter.TraceError( + Utility::FormatString( + L"Key %S not found in JSON object", key.c_str() + ).c_str() + ); + } - // Return an empty string if the key is not found or the value is not a string. return ""; } diff --git a/LogMonitor/src/LogMonitor/JsonProcessor.h b/LogMonitor/src/LogMonitor/JsonProcessor.h index fd778edd..6b197a94 100644 --- a/LogMonitor/src/LogMonitor/JsonProcessor.h +++ b/LogMonitor/src/LogMonitor/JsonProcessor.h @@ -6,25 +6,25 @@ #pragma once bool handleEventLog( - _In_ const boost::json::value& source, + _In_ const nlohmann::json& source, _In_ AttributesMap& Attributes, _Inout_ std::vector>& Sources ); bool handleFileLog( - _In_ const boost::json::value& source, + _In_ const nlohmann::json& source, _In_ AttributesMap& Attributes, _Inout_ std::vector>& Sources ); bool handleETWLog( - _In_ const boost::json::value& source, + _In_ const nlohmann::json& source, _In_ AttributesMap& Attributes, _Inout_ std::vector>& Sources ); bool handleProcessLog( - _In_ const boost::json::value& source, + _In_ const nlohmann::json& source, _In_ AttributesMap& Attributes, _Inout_ std::vector>& Sources ); @@ -39,12 +39,12 @@ std::string readJsonFromFile( ); bool processLogConfig( - const boost::json::value& logConfig, + const nlohmann::json& logConfig, _Out_ LoggerSettings& Config ); bool processSources( - const boost::json::array& sources, + _In_ const nlohmann::json& sources, _Out_ LoggerSettings& Config ); @@ -53,6 +53,7 @@ void cleanupAttributes( ); std::string getJsonStringCaseInsensitive( - _In_ const boost::json::object& obj, - _In_ const std::string& key -); + _In_ const nlohmann::json& obj, + _In_ const std::string& key, + _In_ bool required = false +); \ No newline at end of file diff --git a/LogMonitor/src/LogMonitor/Utility.cpp b/LogMonitor/src/LogMonitor/Utility.cpp index 69b86dea..2adf252b 100644 --- a/LogMonitor/src/LogMonitor/Utility.cpp +++ b/LogMonitor/src/LogMonitor/Utility.cpp @@ -7,6 +7,7 @@ #include using namespace std; +using json = nlohmann::json; /// @@ -265,47 +266,34 @@ bool Utility::isJsonNumber(_In_ std::wstring& str) /// void Utility::SanitizeJson(_Inout_ std::wstring& str) { - size_t i = 0; - while (i < str.size()) { - auto sub = str.substr(i, 1); - auto s = str.substr(0, i + 1); - if (sub == L"\"") { - if ((i > 0 && str.substr(i - 1, 1) != L"\\" && str.substr(i - 1, 1) != L"~") - || i == 0) - { - str.replace(i, 1, L"\\\""); - i++; - } else if (i > 0 && str.substr(i - 1, 1) == L"~") { - str.replace(i - 1, 1, L""); - i--; - } - } - else if (sub == L"\\") { - if ((i < str.size() - 1 && str.substr(i + 1, 1) != L"\\") - || i == str.size() - 1) - { - str.replace(i, 1, L"\\\\"); - i++; - } - else { - i += 2; - } - } - else if (sub == L"\n") { - if (i == 0 || str.substr(i - 1, 1) != L"\\") { - str.replace(i, 1, L"\\n"); - i++; - } - } - else if (sub == L"\r") { - if ((i > 0 && str.substr(i - 1, 1) != L"\\") - || i == 0) - { - str.replace(i, 1, L"\\r"); - i++; - } + try + { + std::string utf8 = Utility::wstring_to_string(str); + + // Remove any embedded nulls + utf8.erase(std::find(utf8.begin(), utf8.end(), '\0'), utf8.end()); + + // Escape the string using JSON + json j = utf8; + std::string escapedUtf8 = j.dump(); + + // Strip the outer quotes + if (escapedUtf8.length() >= 2 && + escapedUtf8.front() == '"' && + escapedUtf8.back() == '"') + { + escapedUtf8 = escapedUtf8.substr(1, escapedUtf8.length() - 2); } - i++; + + // Convert back to wide string + str = Utility::string_to_wstring(escapedUtf8); + } + catch (const json::exception& e) + { + // Leave str unchanged — emitting unescaped content is better than losing the log line. + logWriter.TraceError( + Utility::FormatString(L"SanitizeJson failed: %S", e.what()).c_str() + ); } } diff --git a/LogMonitor/src/LogMonitor/pch.h b/LogMonitor/src/LogMonitor/pch.h index 9612d3c3..b118b3ca 100644 --- a/LogMonitor/src/LogMonitor/pch.h +++ b/LogMonitor/src/LogMonitor/pch.h @@ -47,7 +47,7 @@ #include "shlwapi.h" #include #include -#include +#include #include "Utility.h" #include "Parser/ConfigFileParser.h" #include "Parser/LoggerSettings.h" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5d981bc5..b58c7f79 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,9 +10,10 @@ pool: variables: solution: '**/*.sln' - buildPlatform: 'x64' # Set to x64 or ARM64 depending on the build you want + buildPlatform: 'x64' buildConfiguration: 'Release' VCPKG_ROOT: '$(Build.SourcesDirectory)\vcpkg' + VCPKG_TRIPLET: 'x64-windows-static' VCPKG_CMAKE_OPTIONS: '-DCMAKE_TOOLCHAIN_FILE=$(VCPKG_ROOT)\scripts\buildsystems\vcpkg.cmake' SDKVersion: '' @@ -28,11 +29,10 @@ jobs: git clone https://github.com/microsoft/vcpkg.git $(VCPKG_ROOT) cd $(VCPKG_ROOT) .\bootstrap-vcpkg.bat - .\vcpkg.exe integrate install # Integrate vcpkg with MSBuild + .\vcpkg.exe integrate install - # Install Boost JSON for the selected platform (x64 or ARM64) - task: PowerShell@2 - displayName: 'Install Boost JSON' + displayName: 'Install nlohmann_json' inputs: targetType: 'inline' script: | @@ -40,38 +40,9 @@ jobs: Write-Error "vcpkg.exe not found. Exiting..." exit 1 } - Write-Host "Installing Boost JSON for platform: $(buildPlatform)..." - if ('$(buildPlatform)' -eq 'x64') { - & "$(VCPKG_ROOT)\vcpkg.exe" install boost-json:x64-windows - } elseif ('$(buildPlatform)' -eq 'ARM64') { - & "$(VCPKG_ROOT)\vcpkg.exe" install boost-json:arm64-windows - } else { - Write-Error "Unsupported build platform: $(buildPlatform)" - exit 1 - } - - # Verify vcpkg integration - - task: PowerShell@2 - displayName: 'Verify vcpkg Integration' - inputs: - targetType: 'inline' - script: | - echo "Verifying vcpkg integration for platform: $(buildPlatform)..." - & "$(VCPKG_ROOT)\vcpkg.exe" integrate install - if ('$(buildPlatform)' -eq 'x64') { - if (!(Test-Path "$(VCPKG_ROOT)\installed\x64-windows\include\boost\json.hpp")) { - Write-Error "Boost JSON header not found for x64. Exiting..." - exit 1 - } - } elseif ('$(buildPlatform)' -eq 'ARM64') { - if (!(Test-Path "$(VCPKG_ROOT)\installed\arm64-windows\include\boost\json.hpp")) { - Write-Error "Boost JSON header not found for ARM64. Exiting..." - exit 1 - } - } - Write-Output "vcpkg integration verified." + Write-Host "Installing nlohmann_json for platform: $(VCPKG_TRIPLET)..." + & "$(VCPKG_ROOT)\vcpkg.exe" install nlohmann-json:$(VCPKG_TRIPLET) - # Get installed Windows SDK version - task: PowerShell@2 displayName: 'Check Installed Windows SDK Version' inputs: @@ -104,13 +75,12 @@ jobs: exit 1 } - # Configure CMake based on selected platform - task: CMake@1 displayName: 'Configure CMake' inputs: workingDirectory: '$(Build.SourcesDirectory)\LogMonitor' cmakeArgs: | - -A $(buildPlatform) -S $(Build.SourcesDirectory)\LogMonitor -B $(Build.BinariesDirectory)\LogMonitor\$(buildPlatform) + -A $(buildPlatform) -S $(Build.SourcesDirectory)\LogMonitor -B $(Build.BinariesDirectory)\LogMonitor\$(buildPlatform) $(VCPKG_CMAKE_OPTIONS) -DVCPKG_TARGET_TRIPLET=$(VCPKG_TRIPLET) - task: CMake@1 displayName: 'Build with CMake' @@ -118,20 +88,17 @@ jobs: workingDirectory: '$(Build.BinariesDirectory)\LogMonitor\$(buildPlatform)' cmakeArgs: '--build . --config $(buildConfiguration) --parallel' - # Component Governance - task: ComponentGovernanceComponentDetection@0 inputs: scanType: 'Register' verbosity: 'Verbose' alertWarningLevel: 'Low' - # Install Visual Studio Test Platform - task: VisualStudioTestPlatformInstaller@1 inputs: packageFeedSelector: 'nugetOrg' versionSelector: 'latestPreRelease' - # Run Tests - task: VSTest@2 inputs: testSelector: 'testAssemblies' @@ -142,7 +109,6 @@ jobs: rerunFailedTests: true rerunMaxAttempts: 3 - # Publish build artifacts - task: PublishPipelineArtifact@1 inputs: targetPath: '$(Build.BinariesDirectory)\LogMonitor\$(buildPlatform)\$(buildConfiguration)\' diff --git a/build.cmd b/build.cmd index 27ea8146..ca7dbc31 100644 --- a/build.cmd +++ b/build.cmd @@ -1 +1,80 @@ -msbuild LogMonitor\LogMonitor.sln /p:platform=x64 /p:configuration=Release \ No newline at end of file +@echo off +setlocal + +REM Set the path to the LogMonitor folder +set PROJECT_DIR=%~dp0LogMonitor + +REM Set up environment variables +set VCPKG_DIR=%~dp0vcpkg +set BUILD_DIR=%~dp0build +set TOOLCHAIN_FILE=%VCPKG_DIR%\scripts\buildsystems\vcpkg.cmake +set BUILD_TYPE=Release +set BUILD_ARCH=x64 +set VCPKG_TRIPLET=%BUILD_ARCH%-windows-static + +REM Step 1: Clone and bootstrap vcpkg if not already done +if not exist "%VCPKG_DIR%\vcpkg.exe" ( + echo === Cloning vcpkg === + git clone https://github.com/microsoft/vcpkg.git "%VCPKG_DIR%" + echo === Bootstrapping vcpkg === + pushd "%VCPKG_DIR%" + .\bootstrap-vcpkg.bat + popd +) + +REM Step 2: Locate cmake — prefer PATH, then VS, then vcpkg's downloaded copy +where cmake >nul 2>&1 +if errorlevel 1 ( + if exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" ( + set "PATH=%PATH%;C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin" + goto cmake_found + ) + for /f "delims=" %%i in ('dir /s /b "%VCPKG_DIR%\downloads\tools\cmake-*\bin\cmake.exe" 2^>nul') do ( + set "PATH=%PATH%;%%~dpi" + goto cmake_found + ) + echo cmake not found. Install cmake or Visual Studio CMake tools. + exit /b 1 +) +:cmake_found + +REM Step 3: Install nlohmann-json dependencies +echo === Installing nlohmann dependencies === +"%VCPKG_DIR%\vcpkg.exe" install nlohmann-json:%VCPKG_TRIPLET% + +if errorlevel 1 ( + echo Failed to install nlohmann-json dependencies. + exit /b 1 +) + +REM Step 4: Create the build directory if it doesn't exist +if not exist "%PROJECT_DIR%\build" ( + mkdir "%PROJECT_DIR%\build" +) + +REM Navigate into the build directory +cd /d "%PROJECT_DIR%\build" + +REM Run CMake configuration with toolchain and triplet +cmake .. ^ + -DCMAKE_TOOLCHAIN_FILE=%TOOLCHAIN_FILE% ^ + -DCMAKE_BUILD_TYPE=%BUILD_TYPE% ^ + -DVCPKG_TARGET_TRIPLET=%VCPKG_TRIPLET% ^ + -A %BUILD_ARCH% + +if errorlevel 1 ( + echo CMake configuration failed. + exit /b 1 +) + +REM Build the project +cmake --build . --config %BUILD_TYPE% --parallel + +if errorlevel 1 ( + echo Build failed. + exit /b 1 +) + +echo === Build completed successfully === + +endlocal From 8cf93d3c9fa225fdc4b189bca4f425129525119e Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Thu, 9 Apr 2026 14:02:22 +0100 Subject: [PATCH 3/4] fix: install vcpkg and nlohmann-json in SDL compliance pipeline The SDL pipeline was failing because nlohmann/json.hpp was not available when MSBuild compiled the project. Added vcpkg setup and nlohmann-json installation steps before the build step. --- .github/workflows/sdl-compliance-pipeline.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/sdl-compliance-pipeline.yml b/.github/workflows/sdl-compliance-pipeline.yml index 16e18a7a..942c358d 100644 --- a/.github/workflows/sdl-compliance-pipeline.yml +++ b/.github/workflows/sdl-compliance-pipeline.yml @@ -70,6 +70,13 @@ jobs: queries: security-and-quality # Default is "security-extended" build-mode: ${{ matrix.build-mode }} + - name: Install vcpkg and nlohmann-json + run: | + git clone https://github.com/microsoft/vcpkg.git vcpkg + .\vcpkg\bootstrap-vcpkg.bat + .\vcpkg\vcpkg.exe install nlohmann-json:x64-windows + .\vcpkg\vcpkg.exe integrate install + # https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference?view=vs-2022#arguments - name: Build LogMonitor run: | From 9c68877ceb821a524e6f0aaa8e7fcb4ff1522de5 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Mon, 13 Apr 2026 13:37:47 +0100 Subject: [PATCH 4/4] fix: address post-merge review issues for nlohmann JSON implementation - Fix boolean attributes stored as wstring* instead of bool* in JsonProcessor, causing incorrect EventFormatMultiLine/StartAtOldestRecord values after parsing - Fix eventFormatMultiLine default from false to true (documented default) - Make channel level optional in EventLog source (defaults to Error) - Fix processSources to skip invalid sources and continue (resilient mode) - Add missing waitInSeconds parsing in handleFileLog - Fix path injection in Main.cpp: validate config filename and anchor to exe directory using GetModuleFileNameW/PathCombineW - Fix %s -> %hs format specifier for narrow string in EtwMonitor.cpp - Rename Utility string conversion functions to PascalCase - Fix CMake toolchain file setup to run before project() call - Fix output directory to use generator expression for multi-config layout - Add JsonProcessorTests covering case-insensitive keys, optional field defaults, waitInSeconds parsing, and invalid source resilience --- LogMonitor/CMakeLists.txt | 25 +-- LogMonitor/LogMonitorTests/CMakeLists.txt | 12 +- .../LogMonitorTests/JsonProcessorTests.cpp | 180 +++++++++++++++++- LogMonitor/src/CMakeLists.txt | 11 +- LogMonitor/src/LogMonitor/EtwMonitor.cpp | 2 +- LogMonitor/src/LogMonitor/JsonProcessor.cpp | 118 ++++++------ LogMonitor/src/LogMonitor/Main.cpp | 42 +++- LogMonitor/src/LogMonitor/Utility.cpp | 10 +- LogMonitor/src/LogMonitor/Utility.h | 4 +- 9 files changed, 307 insertions(+), 97 deletions(-) diff --git a/LogMonitor/CMakeLists.txt b/LogMonitor/CMakeLists.txt index b44b0d51..7b96c51e 100644 --- a/LogMonitor/CMakeLists.txt +++ b/LogMonitor/CMakeLists.txt @@ -1,25 +1,18 @@ cmake_minimum_required(VERSION 3.15) +# Set vcpkg toolchain before project() so CMake's compiler detection uses it. +# Callers may also pass -DCMAKE_TOOLCHAIN_FILE on the command line (takes precedence). +if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) + set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + CACHE STRING "Vcpkg toolchain file") +endif() + +set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "Vcpkg target triplet") + # Define the project project(LogMonitor) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") -set(Boost_USE_STATIC_LIBS ON) -set(Boost_USE_STATIC_RUNTIME ON) - -# Set C++ standard -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_VERBOSE_MAKEFILE ON) -set(CMAKE_BUILD_TYPE Release) -set(VCPKG_TARGET_TRIPLET x64-windows-static) - -# Use vcpkg if available -if(DEFINED ENV{VCPKG_ROOT}) - set(VCPKG_ROOT $ENV{VCPKG_ROOT}) - set(CMAKE_TOOLCHAIN_FILE "${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file" FORCE) - set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "Vcpkg target triplet") -endif() # Enforce static MSVC runtime (/MT or /MTd) if(MSVC) diff --git a/LogMonitor/LogMonitorTests/CMakeLists.txt b/LogMonitor/LogMonitorTests/CMakeLists.txt index 1b7a63be..a06236fc 100644 --- a/LogMonitor/LogMonitorTests/CMakeLists.txt +++ b/LogMonitor/LogMonitorTests/CMakeLists.txt @@ -26,17 +26,17 @@ target_include_directories(LogMonitorTests PRIVATE ) # Set Windows-specific linker flags +# Use $ so binaries land in Release/ or Debug/, matching the pipeline's $(buildConfiguration) layout. set_target_properties(LogMonitorTests PROPERTIES COMPILE_PDB_NAME "LogMonitorTests" - COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" LINK_FLAGS "/DEBUG:FULL /OPT:REF /OPT:ICF" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" ) # Link LogMonitor and Nlohmann JSON target_link_libraries(LogMonitorTests PRIVATE LogMonitorLib nlohmann_json::nlohmann_json) -# Enable testing -enable_testing() - -add_test(NAME LogMonitorTests COMMAND LogMonitorTests) +# LogMonitorTests is a shared library (DLL) for VSTest, not a standalone executable. +# Run tests with: vstest.console.exe /Release/LogMonitorTests.dll +# (as used by the Azure pipeline) diff --git a/LogMonitor/LogMonitorTests/JsonProcessorTests.cpp b/LogMonitor/LogMonitorTests/JsonProcessorTests.cpp index 5c223e72..502e3ef4 100644 --- a/LogMonitor/LogMonitorTests/JsonProcessorTests.cpp +++ b/LogMonitor/LogMonitorTests/JsonProcessorTests.cpp @@ -4,20 +4,192 @@ // #include "pch.h" - +#include "../src/LogMonitor/JsonProcessor.h" using namespace Microsoft::VisualStudio::CppUnitTestFramework; #define BUFFER_SIZE 65536 -namespace UtilityTests +namespace LogMonitorTests { /// - /// Tests of Utility class methods. + /// Tests of the JsonProcessor's ReadConfigFile function, which replaced the + /// Boost-based ConfigFileParser. Covers source parsing, optional-field + /// defaults, case-insensitive key matching, waitInSeconds, and invalid-input + /// rejection. /// TEST_CLASS(JsonProcessorTests) { + WCHAR bigOutBuf[BUFFER_SIZE]; + std::wstring tempFilePath; + + std::wstring RecoverOutput() + { + return std::wstring(bigOutBuf); + } + + std::wstring WriteTempConfig(const std::string& content) + { + WCHAR tempDir[MAX_PATH] = {}; + GetTempPathW(MAX_PATH, tempDir); + WCHAR tempFile[MAX_PATH] = {}; + GetTempFileNameW(tempDir, L"jpt", 0, tempFile); + tempFilePath = tempFile; + + std::ofstream f(tempFile, std::ios::binary); + f << content; + return tempFilePath; + } + public: - + + TEST_METHOD_INITIALIZE(Initialize) + { + ZeroMemory(bigOutBuf, sizeof(bigOutBuf)); + fflush(stdout); + _setmode(_fileno(stdout), _O_U16TEXT); + setvbuf(stdout, (char*)bigOutBuf, _IOFBF, sizeof(bigOutBuf) - sizeof(WCHAR)); + tempFilePath.clear(); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + if (!tempFilePath.empty()) + { + DeleteFileW(tempFilePath.c_str()); + } + } + + /// + /// Channel name/level lookup must be case-insensitive for backward + /// compatibility with existing configs. + /// + TEST_METHOD(JsonProcessor_HandlesCaseInsensitiveKeys) + { + auto path = WriteTempConfig(R"({ + "LogConfig": { + "sources": [{ + "type": "EventLog", + "channels": [{"Name": "system", "Level": "Warning"}] + }] + } + })"); + + LoggerSettings settings; + bool success = ReadConfigFile((PWCHAR)path.c_str(), settings); + + Assert::IsTrue(success); + Assert::AreEqual((size_t)1, settings.Sources.size()); + + auto src = std::reinterpret_pointer_cast(settings.Sources[0]); + Assert::AreEqual(std::wstring(L"system"), src->Channels[0].Name); + Assert::AreEqual((int)EventChannelLogLevel::Warning, (int)src->Channels[0].Level); + } + + /// + /// Optional fields omitted from the config must use their documented + /// default values so existing configs are not broken. + /// + TEST_METHOD(JsonProcessor_UsesDefaultValuesForOptionalFields) + { + // EventLog: startAtOldestRecord defaults false, eventFormatMultiLine + // defaults true, channel level defaults Error. + auto path = WriteTempConfig(R"({ + "LogConfig": { + "sources": [{ + "type": "EventLog", + "channels": [{"name": "system"}] + }] + } + })"); + + LoggerSettings settings; + bool success = ReadConfigFile((PWCHAR)path.c_str(), settings); + + Assert::IsTrue(success); + Assert::AreEqual((size_t)1, settings.Sources.size()); + + auto src = std::reinterpret_pointer_cast(settings.Sources[0]); + Assert::IsFalse(src->StartAtOldestRecord); + Assert::IsTrue(src->EventFormatMultiLine); + Assert::AreEqual((int)EventChannelLogLevel::Error, (int)src->Channels[0].Level); + } + + /// + /// waitInSeconds on a File source must be parsed and stored correctly. + /// When omitted the default value (300) must be used. + /// + TEST_METHOD(JsonProcessor_ParsesWaitInSeconds) + { + // Explicit value + auto path = WriteTempConfig(R"({ + "LogConfig": { + "sources": [{ + "type": "File", + "directory": "C:\\logs", + "filter": "*.log", + "includeSubdirectories": false, + "waitInSeconds": 60 + }] + } + })"); + + LoggerSettings settings; + bool success = ReadConfigFile((PWCHAR)path.c_str(), settings); + + Assert::IsTrue(success); + Assert::AreEqual((size_t)1, settings.Sources.size()); + + auto src = std::reinterpret_pointer_cast(settings.Sources[0]); + Assert::AreEqual(60.0, src->WaitInSeconds); + + // Default value when omitted + auto path2 = WriteTempConfig(R"({ + "LogConfig": { + "sources": [{ + "type": "File", + "directory": "C:\\logs", + "filter": "*.log", + "includeSubdirectories": false + }] + } + })"); + + LoggerSettings settings2; + bool success2 = ReadConfigFile((PWCHAR)path2.c_str(), settings2); + + Assert::IsTrue(success2); + auto src2 = std::reinterpret_pointer_cast(settings2.Sources[0]); + Assert::AreEqual(300.0, src2->WaitInSeconds); + } + + /// + /// A source with an unknown type must be skipped with an error logged, + /// but valid sources in the same config must still be processed. + /// + TEST_METHOD(JsonProcessor_RejectsInvalidSource) + { + auto path = WriteTempConfig(R"({ + "LogConfig": { + "sources": [ + {"type": "InvalidSourceType"}, + { + "type": "File", + "directory": "C:\\logs", + "filter": "*.log", + "includeSubdirectories": false + } + ] + } + })"); + + LoggerSettings settings; + bool success = ReadConfigFile((PWCHAR)path.c_str(), settings); + + // Overall parse succeeds — invalid source is skipped, valid one is kept + Assert::IsTrue(success); + Assert::AreEqual((size_t)1, settings.Sources.size()); + Assert::AreEqual((int)LogSourceType::File, (int)settings.Sources[0]->Type); + } }; } diff --git a/LogMonitor/src/CMakeLists.txt b/LogMonitor/src/CMakeLists.txt index c8c9e60d..35ca5752 100644 --- a/LogMonitor/src/CMakeLists.txt +++ b/LogMonitor/src/CMakeLists.txt @@ -37,10 +37,11 @@ target_include_directories(LogMonitor PRIVATE target_link_libraries(LogMonitorLib PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(LogMonitor PRIVATE LogMonitorLib nlohmann_json::nlohmann_json) -# Set output path +# Set output path — use $ so Release/Debug binaries go into separate subdirectories, +# matching the pipeline's $(buildConfiguration) layout. set_target_properties(LogMonitor PROPERTIES - OUTPUT_NAME "LogMonitor" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" - ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + OUTPUT_NAME "LogMonitor" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" ) diff --git a/LogMonitor/src/LogMonitor/EtwMonitor.cpp b/LogMonitor/src/LogMonitor/EtwMonitor.cpp index 6630e483..571c7328 100644 --- a/LogMonitor/src/LogMonitor/EtwMonitor.cpp +++ b/LogMonitor/src/LogMonitor/EtwMonitor.cpp @@ -663,7 +663,7 @@ EtwMonitor::OnRecordEvent( } catch (std::bad_alloc& e) { logWriter.TraceError( - Utility::FormatString(L"Failed to allocate memory for event info (size=%lu): %s", bufferSize, e.what()).c_str() + Utility::FormatString(L"Failed to allocate memory for event info (size=%lu): %hs", bufferSize, e.what()).c_str() ); status = ERROR_OUTOFMEMORY; } diff --git a/LogMonitor/src/LogMonitor/JsonProcessor.cpp b/LogMonitor/src/LogMonitor/JsonProcessor.cpp index a0cd16e0..bc49ed4e 100644 --- a/LogMonitor/src/LogMonitor/JsonProcessor.cpp +++ b/LogMonitor/src/LogMonitor/JsonProcessor.cpp @@ -71,21 +71,27 @@ bool ReadConfigFile(_In_ const PWCHAR jsonFile, _Out_ LoggerSettings& Config) { /// A UTF-8 encoded string containing the JSON file's contents, /// or an empty string if the file could not be opened. std::string readJsonFromFile(_In_ const std::wstring& filePath) { - // Open the file as a wide character input stream - std::wifstream wif(filePath); - if (!wif.is_open()) { + // Read raw bytes so UTF-8 content is preserved exactly as stored. + std::ifstream input(filePath, std::ios::binary); + if (!input.is_open()) { logWriter.TraceError(L"Failed to open JSON file."); return ""; - } - - // Read the file into a wide string buffer - std::wstringstream wss; - wss << wif.rdbuf(); - wif.close(); + } - // Convert the wstring buffer to a UTF-8 string - std::wstring_convert> converter; - std::string jsonString = converter.to_bytes(wss.str()); + std::string jsonString( + (std::istreambuf_iterator(input)), + std::istreambuf_iterator() + ); + input.close(); + + // Strip UTF-8 BOM if present. + if (jsonString.size() >= 3 && + static_cast(jsonString[0]) == 0xEF && + static_cast(jsonString[1]) == 0xBB && + static_cast(jsonString[2]) == 0xBF) + { + jsonString.erase(0, 3); + } return jsonString; } @@ -111,7 +117,7 @@ bool handleEventLog( startAtOldest = source["startAtOldestRecord"].get(); } - bool multiLine = false; + bool multiLine = true; if (source.contains("eventFormatMultiLine") && source["eventFormatMultiLine"].is_boolean()) { multiLine = source["eventFormatMultiLine"].get(); } @@ -122,15 +128,15 @@ bool handleEventLog( } Attributes[JSON_TAG_START_AT_OLDEST_RECORD] = reinterpret_cast( - std::make_unique(startAtOldest ? L"true" : L"false").release() + std::make_unique(startAtOldest).release() ); Attributes[JSON_TAG_FORMAT_MULTILINE] = reinterpret_cast( - std::make_unique(multiLine ? L"true" : L"false").release() + std::make_unique(multiLine).release() ); Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( - std::make_unique(Utility::string_to_wstring(customLogFormat)).release() + std::make_unique(Utility::StringToWString(customLogFormat)).release() ); // Process channels if they exist @@ -139,20 +145,22 @@ bool handleEventLog( for (const auto& channel : source["channels"]) { std::string name = getJsonStringCaseInsensitive(channel, "name", true); - std::string levelString = getJsonStringCaseInsensitive(channel, "level", true); - - EventLogChannel eventChannel(Utility::string_to_wstring(name), EventChannelLogLevel::Error); - - std::wstring level = Utility::string_to_wstring(levelString); - // Set the level based on the levelString, logging an error if invalid - if (!eventChannel.SetLevelByString(level)) { - logWriter.TraceError( - Utility::FormatString( - L"Invalid level string: %S", - levelString.c_str() - ).c_str() - ); - return false; + std::string levelString = getJsonStringCaseInsensitive(channel, "level"); + + EventLogChannel eventChannel(Utility::StringToWString(name), EventChannelLogLevel::Error); + + if (!levelString.empty()) { + std::wstring level = Utility::StringToWString(levelString); + // Set the level based on the levelString, logging an error if invalid + if (!eventChannel.SetLevelByString(level)) { + logWriter.TraceError( + Utility::FormatString( + L"Invalid level string: %S", + levelString.c_str() + ).c_str() + ); + return false; + } } channels->push_back(eventChannel); @@ -208,18 +216,24 @@ bool handleFileLog( std::string customLogFormat = getJsonStringCaseInsensitive(source, "customLogFormat"); Attributes[JSON_TAG_DIRECTORY] = reinterpret_cast( - std::make_unique(Utility::string_to_wstring(directory)).release() + std::make_unique(Utility::StringToWString(directory)).release() ); Attributes[JSON_TAG_FILTER] = reinterpret_cast( - std::make_unique(Utility::string_to_wstring(filter)).release() + std::make_unique(Utility::StringToWString(filter)).release() ); Attributes[JSON_TAG_INCLUDE_SUBDIRECTORIES] = reinterpret_cast( - std::make_unique(includeSubdirs ? L"true" : L"false").release() + std::make_unique(includeSubdirs).release() ); Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( - std::make_unique(Utility::string_to_wstring(customLogFormat)).release() + std::make_unique(Utility::StringToWString(customLogFormat)).release() ); + if (source.contains("waitInSeconds") && source["waitInSeconds"].is_number()) { + Attributes[JSON_TAG_WAITINSECONDS] = reinterpret_cast( + std::make_unique(source["waitInSeconds"].get()).release() + ); + } + auto sourceFile = std::make_shared(); if (!SourceFile::Unwrap(Attributes, *sourceFile)) { logWriter.TraceError(L"Error parsing configuration file. Invalid File source"); @@ -255,12 +269,12 @@ bool handleETWLog( customLogFormat = source["customLogFormat"].get(); } - // Store multiLine and customLogFormat as wide strings in Attributes + // Store multiLine and customLogFormat in Attributes Attributes[JSON_TAG_FORMAT_MULTILINE] = reinterpret_cast( - std::make_unique(multiLine ? L"true" : L"false").release() + std::make_unique(multiLine).release() ); Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( - std::make_unique(Utility::string_to_wstring(customLogFormat)).release() + std::make_unique(Utility::StringToWString(customLogFormat)).release() ); std::vector etwProviders; @@ -272,14 +286,14 @@ bool handleETWLog( std::string level = getJsonStringCaseInsensitive(provider, "level", true); ETWProvider etwProvider; - etwProvider.ProviderName = Utility::string_to_wstring(providerName); - etwProvider.SetProviderGuid(Utility::string_to_wstring(providerGuid)); - etwProvider.StringToLevel(Utility::string_to_wstring(level)); + etwProvider.ProviderName = Utility::StringToWString(providerName); + etwProvider.SetProviderGuid(Utility::StringToWString(providerGuid)); + etwProvider.StringToLevel(Utility::StringToWString(level)); // Check if "keywords" exists and process it if (provider.contains("keywords") && provider["keywords"].is_string()) { std::string keywords = provider["keywords"].get(); - etwProvider.Keywords = wcstoull(Utility::string_to_wstring(keywords).c_str(), nullptr, 0); + etwProvider.Keywords = wcstoull(Utility::StringToWString(keywords).c_str(), nullptr, 0); } etwProviders.push_back(std::move(etwProvider)); @@ -326,7 +340,7 @@ bool handleProcessLog( } Attributes[JSON_TAG_CUSTOM_LOG_FORMAT] = reinterpret_cast( - std::make_unique(Utility::string_to_wstring(customLogFormat)).release() + std::make_unique(Utility::StringToWString(customLogFormat)).release() ); auto sourceProcess = std::make_shared(); @@ -363,7 +377,7 @@ bool processLogConfig(_In_ const nlohmann::json& logConfig, _Out_ LoggerSettings } if (!logFormat.empty()) { - Config.LogFormat = Utility::string_to_wstring(logFormat); + Config.LogFormat = Utility::StringToWString(logFormat); } else { logWriter.TraceError(L"LogFormat not found in LogConfig. Using default log format."); } @@ -385,12 +399,12 @@ bool processLogConfig(_In_ const nlohmann::json& logConfig, _Out_ LoggerSettings /// JSON array containing different log sources. /// LoggerSettings structure where parsed sources are stored. /// -/// Returns true if all sources are successfully processed; -/// otherwise, returns false if any source fails to process. +/// Returns true if the sources array is structurally valid (even if individual +/// source entries fail to parse). Invalid entries are logged and skipped so that +/// well-formed sources can still start, matching the previous parser's resilience. +/// Returns false only if sources is not an array. /// bool processSources(_In_ const nlohmann::json& sources, _Out_ LoggerSettings& Config) { - bool overallSuccess = true; - if (!sources.is_array()) { logWriter.TraceError(L"Sources is not an array."); return false; @@ -399,7 +413,6 @@ bool processSources(_In_ const nlohmann::json& sources, _Out_ LoggerSettings& Co for (const auto& source : sources) { if (!source.is_object()) { logWriter.TraceError(L"Skipping invalid source entry (not an object)."); - overallSuccess = false; continue; } @@ -412,7 +425,6 @@ bool processSources(_In_ const nlohmann::json& sources, _Out_ LoggerSettings& Co if (sourceType.empty()) { logWriter.TraceError(L"Skipping source with missing or empty type."); - overallSuccess = false; continue; } @@ -431,10 +443,9 @@ bool processSources(_In_ const nlohmann::json& sources, _Out_ LoggerSettings& Co logWriter.TraceError( Utility::FormatString( L"Invalid source type: %S", - Utility::string_to_wstring(sourceType).c_str() + Utility::StringToWString(sourceType).c_str() ).c_str() ); - overallSuccess = false; continue; } @@ -442,16 +453,15 @@ bool processSources(_In_ const nlohmann::json& sources, _Out_ LoggerSettings& Co logWriter.TraceError( Utility::FormatString( L"Failed to process source of type: %S", - Utility::string_to_wstring(sourceType).c_str() + Utility::StringToWString(sourceType).c_str() ).c_str() ); - overallSuccess = false; } cleanupAttributes(sourceAttributes); } - return overallSuccess; + return true; } /// diff --git a/LogMonitor/src/LogMonitor/Main.cpp b/LogMonitor/src/LogMonitor/Main.cpp index 1b35766f..bfe50f95 100644 --- a/LogMonitor/src/LogMonitor/Main.cpp +++ b/LogMonitor/src/LogMonitor/Main.cpp @@ -259,9 +259,9 @@ void StartMonitors(_In_ LoggerSettings& settings) std::vector eventChannels; std::vector etwProviders; - bool eventMonMultiLine; - bool eventMonStartAtOldestRecord; - bool etwMonMultiLine; + bool eventMonMultiLine = false; + bool eventMonStartAtOldestRecord = false; + bool etwMonMultiLine = false; // Set the log format from settings logFormat = settings.LogFormat; @@ -341,6 +341,7 @@ int __cdecl wmain(int argc, WCHAR *argv[]) { std::wstring cmdline; PWCHAR configFileName = (PWCHAR)DEFAULT_CONFIG_FILENAME; + std::wstring resolvedConfigPath = DEFAULT_CONFIG_FILENAME; int exitcode = 0; g_hStopEvent = CreateEvent(nullptr, // default security attributes @@ -375,13 +376,46 @@ int __cdecl wmain(int argc, WCHAR *argv[]) if (_wcsnicmp(argv[1], ARGV_OPTION_CONFIG_FILE, _countof(ARGV_OPTION_CONFIG_FILE)) == 0) { configFileName = argv[2]; + std::wstring userConfigName = argv[2]; + + // Reject paths with traversal sequences or absolute path indicators + if (userConfigName.find(L"..") != std::wstring::npos || + userConfigName.find(L'/') != std::wstring::npos || + userConfigName.find(L'\\') != std::wstring::npos || + userConfigName.find(L':') != std::wstring::npos) + { + logWriter.TraceError(L"Invalid configuration file name."); + return 0; + } + + // Anchor the config file to the executable's directory + WCHAR modulePath[MAX_PATH] = {}; + if (GetModuleFileNameW(nullptr, modulePath, _countof(modulePath)) == 0) + { + logWriter.TraceError( + Utility::FormatString(L"Failed to get module path. Error: %d", GetLastError()).c_str() + ); + return 0; + } + if (!PathRemoveFileSpecW(modulePath)) + { + logWriter.TraceError(L"Failed to resolve executable directory."); + return 0; + } + WCHAR combinedPath[MAX_PATH] = {}; + if (PathCombineW(combinedPath, modulePath, userConfigName.c_str()) == nullptr) + { + logWriter.TraceError(L"Failed to resolve configuration file path."); + return 0; + } + resolvedConfigPath = combinedPath; indexCommandArgument = 3; } } LoggerSettings settings; //read the config file - bool configFileReadSuccess = ReadConfigFile(configFileName, settings); + bool configFileReadSuccess = ReadConfigFile((PWCHAR)resolvedConfigPath.c_str(), settings); //start the monitors if (configFileReadSuccess) diff --git a/LogMonitor/src/LogMonitor/Utility.cpp b/LogMonitor/src/LogMonitor/Utility.cpp index 2adf252b..3916ce52 100644 --- a/LogMonitor/src/LogMonitor/Utility.cpp +++ b/LogMonitor/src/LogMonitor/Utility.cpp @@ -268,7 +268,7 @@ void Utility::SanitizeJson(_Inout_ std::wstring& str) { try { - std::string utf8 = Utility::wstring_to_string(str); + std::string utf8 = Utility::WStringToString(str); // Remove any embedded nulls utf8.erase(std::find(utf8.begin(), utf8.end(), '\0'), utf8.end()); @@ -286,9 +286,9 @@ void Utility::SanitizeJson(_Inout_ std::wstring& str) } // Convert back to wide string - str = Utility::string_to_wstring(escapedUtf8); + str = Utility::StringToWString(escapedUtf8); } - catch (const json::exception& e) + catch (const std::exception& e) { // Leave str unchanged — emitting unescaped content is better than losing the log line. logWriter.TraceError( @@ -431,7 +431,7 @@ bool Utility::IsCustomJsonFormat(_Inout_ std::wstring& customLogFormat) /// /// /// -std::string Utility::wstring_to_string(_In_ const std::wstring& wstr) { +std::string Utility::WStringToString(_In_ const std::wstring& wstr) { std::wstring_convert> converter; return converter.to_bytes(wstr); } @@ -441,7 +441,7 @@ std::string Utility::wstring_to_string(_In_ const std::wstring& wstr) { /// /// The input string to be converted /// A wide string representation of the input string -std::wstring Utility::string_to_wstring(_In_ const std::string& str) { +std::wstring Utility::StringToWString(_In_ const std::string& str) { std::wstring_convert> converter; return converter.from_bytes(str); } diff --git a/LogMonitor/src/LogMonitor/Utility.h b/LogMonitor/src/LogMonitor/Utility.h index 37155671..99e30fe9 100644 --- a/LogMonitor/src/LogMonitor/Utility.h +++ b/LogMonitor/src/LogMonitor/Utility.h @@ -91,7 +91,7 @@ class Utility final static bool IsCustomJsonFormat(_Inout_ std::wstring& customLogFormat); - static std::string wstring_to_string(_In_ const std::wstring& wstr); + static std::string WStringToString(_In_ const std::wstring& wstr); - static std::wstring string_to_wstring(_In_ const std::string& str); + static std::wstring StringToWString(_In_ const std::string& str); };