diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1ba5c79 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,364 @@ +root = true + +# All files +[*] +indent_style = space + +# Xml files +[*.xml] +indent_size = 2 + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 2 +tab_width = 2 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### +[*.{cs,vb}] + +# Organize usings +dotnet_separate_import_directive_groups = true +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +#### C# Coding Conventions #### +[*.cs] + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### +[*.{cs,vb}] + +# Naming rules + +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase + +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase + +dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase + +dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase + +dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = camelcase + +dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +# Symbol specifications + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f074404 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.txt +**/obj/* +**/bin/* +test/**/coveragereport/* +test/**/coveragereporthistory/* +test/**/TestResults/* +out/* +*.zip diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..da4ad37 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build - Debug", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/EventLogMonitor/bin/Debug/net6.0-windows/EventLogMonitor.dll", + // "args": ["-p", "10", "-c", "De-DE", "-s", "*", "-v", "-l", "OAlerts"], + // "args": ["-p", "*", "-i", "256688", "-b1", "-nt"], + // "args": ["-l2", "System,Windows PowerShell", "-d"], + // "args": ["-p", "30", "-nt", "-l" , "Windows PowerShell"], + "args": ["-p", "3", "-nt", "-l" , "System", "-s", "Http", "-c", "en-DE"], + // "args":["-p", "*", "-3", "-l", "Security", "-fi", "Windows Firewall did not apply the following rule"], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1fafba6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,191 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build - Debug", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/EventLogMonitor/EventLogMonitor.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "build - Release", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/EventLogMonitor/EventLogMonitor.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "-c", + "Release" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "publish - Release as a single file WITH runtime", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/EventLogMonitor/EventLogMonitor.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "-c", + "Release", + "-r", + "win-x64", + "/p:PublishSingleFile=true", + "/p:IncludeNativeLibrariesForSelfExtract=true", + "--self-contained", + "true", + "-p:PublishReadyToRun=true" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish - Release as a single file WITHOUT runtime", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/EventLogMonitor/EventLogMonitor.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "-c", + "Release", + "-r", + "win-x64", + "/p:PublishSingleFile=true", + "/p:IncludeNativeLibrariesForSelfExtract=true", + "--self-contained", + "false", + "-p:PublishReadyToRun=true" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "clean - Debug", + "command": "dotnet", + "type": "process", + "args": [ + "clean", + "${workspaceFolder}/src/EventLogMonitor/EventLogMonitor.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "-c", + "Debug" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "clean - Release", + "command": "dotnet", + "type": "process", + "args": [ + "clean", + "${workspaceFolder}/src/EventLogMonitor/EventLogMonitor.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "-c", + "Release" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch - Debug Build", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "build", + "--project", + "${workspaceFolder}/src/EventLogMonitor/EventLogMonitor.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch - Debug Tests", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "test", + "--project", + "${workspaceFolder}/test/EventLogMonitorTests/EventLogMonitorTests.csproj", + ], + "problemMatcher": "$msCompile" + }, + { + "label": "run tests against Release build", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "-c", + "Release" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "run tests with code coverage", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "--collect:\"XPlat Code Coverage\"", + "--results-directory=./test/EventLogMonitorTests/TestResults/CoverageResults/" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "generate html code coverage report", + "command": "reportgenerator", + "type": "process", + "args": [ + "-reports:\"./test/EventLogMonitorTests/TestResults/CoverageResults/*/coverage.cobertura.xml\"", + "-targetdir:\"test/EventLogMonitorTests/coveragereport\"", + "-historydir:\"test/EventLogMonitorTests/coveragereporthistory\"", + "-title:EventLogMonitor", + "-reporttypes:Html" + ], + "dependsOn": [ + "run tests with code coverage" + ], + "problemMatcher": [] + }, + { + "label": "generate and view html code coverage report in default browser", + "command": "explorer", + "type": "process", + "args": [ + "${workspaceFolder}\\test\\EventLogMonitorTests\\coveragereport\\index.html" + ], + "dependsOn": [ + "generate html code coverage report" + ], + "problemMatcher": [] + }, + { + "label": "view most recent html code coverage report in default browser", + "command": "explorer", + "type": "process", + "args": [ + "${workspaceFolder}\\test\\EventLogMonitorTests\\coveragereport\\index.html" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/EventLogMonitor.sln b/EventLogMonitor.sln new file mode 100644 index 0000000..db59d8f --- /dev/null +++ b/EventLogMonitor.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1AAD5DB6-6B6A-4389-B3D5-BA1AFA0493DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventLogMonitor", "src\EventLogMonitor\EventLogMonitor.csproj", "{EC6C0C45-7884-465B-A656-F56B3D9BA316}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8F3C2DF1-3BC6-45BD-9F52-88BF3AD419F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventLogMonitorTests", "test\EventLogMonitorTests\EventLogMonitorTests.csproj", "{0E0237E3-85FD-4D63-A44C-38D9AC20CEEB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EC6C0C45-7884-465B-A656-F56B3D9BA316}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC6C0C45-7884-465B-A656-F56B3D9BA316}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC6C0C45-7884-465B-A656-F56B3D9BA316}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC6C0C45-7884-465B-A656-F56B3D9BA316}.Release|Any CPU.Build.0 = Release|Any CPU + {0E0237E3-85FD-4D63-A44C-38D9AC20CEEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E0237E3-85FD-4D63-A44C-38D9AC20CEEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E0237E3-85FD-4D63-A44C-38D9AC20CEEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E0237E3-85FD-4D63-A44C-38D9AC20CEEB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {EC6C0C45-7884-465B-A656-F56B3D9BA316} = {1AAD5DB6-6B6A-4389-B3D5-BA1AFA0493DD} + {0E0237E3-85FD-4D63-A44C-38D9AC20CEEB} = {8F3C2DF1-3BC6-45BD-9F52-88BF3AD419F2} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE index 261eeb9..3621f36 100644 --- a/LICENSE +++ b/LICENSE @@ -174,28 +174,6 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + - APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index 84dbe33..57b1895 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,399 @@ # EventLogMonitor -A command line Event Log monitor for Windows that allows queries and tailing +EventLogMonitor is a tool that allows you to view and tail events from the Windows Event Log at the command line. + +This tool was originally written around 10 years ago to work with the IBM WebSphere Message Broker product and subsequent releases, and this is reflected in some of the defaults the tool still uses today. However, it also can be used to monitor events written by any program that writes to the Event Log. +## Installation +No real installation is required - simply unzip the download and copy the **EventLogMonitor.exe** application and **EventLogMonitor.pdb** into a folder on your path. There are two versions of the tool to choose from + * a "smaller" **EventLogMonitor-vX-without-framework.zip** version that requires that you have the [.NET 6 Runtime framework](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) installed as a pre-req. + * a "larger" **EventLogMonitor-vX-with-framework.zip** version that is self-contained and does not require any pre-reqs. + + Simply pick the latest version that matches your environment from the [releases](https://github.com/m-g-k/EventLogMonitor/releases) page on GitHub. The functionality of both versions is the same. + +## Usage +There are three different ways to use this tool. The first is to simply query all events from the Application Event Log like this: + +`EventLogMonitor.exe -s *`
+this should give you the following output:
+ +`Waiting for events from the Application log matching the event source '*'. `
+`Press , 'Q' or to exit or press 'S' for current stats...`
+ +As you can see this is automatically tailing the Application event log, waiting for events to be written from any event source. You can narrow down the sources if necessary to avoid seeing events that are not relevant to you by specifying them instead of the asterisk like this:
+ +`EventLogMonitor.exe -s Firefox`
+`Waiting for events from the Application log matching the event source 'Firefox'.`
+`Press , 'Q' or to exit or press 'S' for current stats...`
+ +As you can see this is now waiting for events written by the event source "Firefox". Note that the source is a lazy match and does not have to be exact. As long as the source contains the string you specify then it will be considered a match. Note that this string is case sensitive. + +## Waiting for events from multiple sources +You can specify multiple sources by separating them with a comma (`,`) like this:
+ +`EventLogMonitor.exe -s Firefox,edge`
+`Waiting for events from the Application log matching the event source 'Firefox' or 'edge'.`
+`Press , 'Q' or to exit or press 'S' for current stats...` + +If you need to specify a source name that has spaces you can enclose the string in quotes like this:
+ +`EventLogMonitor.exe -s "Firefox Default Browser Agent,edge"`
+`Waiting for events from the Application log matching the event source 'Firefox Default Browser Agent' or 'edge'.`
+`Press , 'Q' or to exit or press 'S' for current stats...` + +Note that the search performed is a case sensitive partial match, so as long as the string specified with `-s` is present in the full event source name then it will match. + +## Choosing the Event Log to view +You can choose the log to view by specifying the log name with `-l`. To change from viewing the default `Application` log to view the `System` log, you would run:
+ +`EventLogMonitor.exe -l System`
+`Waiting for events from the System log matching the event source '*'.`
+`Press , 'Q' or to exit or press 'S' for current stats...` + +Of course, you can also combine `-l` with `-s` as you would expect to view a specific source or multiple sources from the chosen log. +## Viewing previous events in an Event Log +When starting to tail a log it is often useful to view a few previous entries that have already been written to the log to understand what has already happened before new events start to appear. This also helps to make sure you have spelt your event source name correctly. To do this we use the `-p` option to display previous events along with a count of how many events should be displayed like this:
+ +`EventLogMonitor.exe -s SPP -p 2`
+**16394I:** `Offline downlevel migration succeeded.` **`[23/01/2022 16:49:56.228]`**
+**16384I:** `Successfully scheduled Software Protection service for re-start at 2121-12-30T16:50:27Z. Reason: RulesEngine.` **`[23/01/2022 16:50:27.196]`**
+ +This time the output is different. Here we can see that two previous events have been written from the `Security-SPP` log before the terminal stops to wait for new events to be written. You would also see that the event numbers are colour coded to allow us to easily identify problems.
+ +* **Information** event numbers are written in green. They are also suffixed with the letter `I` for easy identification. +* **Warning** event numbers are written in yellow. They are also suffixed with the letter `W` for easy identification. +* **Error** event numbers are written in red. They are also suffixed with the letter `E` for easy identification.
+* **Critical** event numbers are written in dark red. They are also suffixed with the letter `C` for easy identification.
+ +We can also see that the timestamp showing when the events were first written to the log is shown at the end in **bold**. If we prefer we can choose to write the timestamp at the beginning of the event's output rather than at the end by specifying the `-tf` or "timestamp-first" option:
+ +`EventLogMonitor.exe -s SPP -p 2 -tf`
+**`23/01/2022 16:49:56.228`**`:` **16394I:** `Offline downlevel migration succeeded.`
+**`23/01/2022 16:50:27.196`**`:` **16384I:** `Successfully scheduled Software Protection service for re-start at 2121-12-30T16:50:27Z. Reason: RulesEngine.`
+ +At this point we can leave the terminal open and wait for more events to appear or press `, 'Q' or ` to stop waiting and quit. We can also press `S` to see the statistics on how many events have been written so far:
+ +`...`
+`2 Entries shown so far from the Application log. Waiting for more events...` + +Note that we can choose to display all previous events by using `-p *`. + +## Displaying available Event Logs +Another way to use this tool is to see what event logs are registered on the system using the `-d` "display-logs" option to output a list of all registered logs:
+ +`EventLogMonitor.exe -d`
+**`LogName : Entries : LastWriteTime : [DisplayName]`**
+**`-------------------------------------------------`**
+**`Windows PowerShell`**` : 8452 : 23/01/2022 16:47:54 : [Windows PowerShell]`
+**`System`**` : 39157 : 23/01/2022 16:54:56 : [System]`
+**`Lenovo-Power-BaseModule/Operational`**` : 505 : 23/01/2022 13:31:48 : [Lenovo-Power-BaseModule]`
+`...`
+`Some providers maybe ignored (not Admin).`
+`141 Providers listed.` + +Here we can see that on this system we have over 141 providers listed, although some additional ones may not have been shown as we were not running this command prompt "elevated" and some logs can only be accessed as an Administrator. If we re-run this command from an elevated prompt, we can see more providers listed. + +By default, this output shows the key pieces of information about each log provider - the log's name, the number of events in the log and when the last entry was written along with a display name which is sometimes different to the log name. + +However, this command will ignore all logs that have zero entries in them. If you want to list every log, including those with no entries, you need to add the `-v` or "verbose" option which also shows extra information about each log:
+ +`EventLogMonitor.exe -d -v`
+**`Windows PowerShell`**
+  `DisplayName: Windows PowerShell`
+  `Records: 8452`
+  `FileSize: 15732736`
+  `IsFull: False`
+  `CreationTime: 26/09/2020 21:29:27`
+  `LastWrite: 23/01/2022 16:47:54`
+  `OldestIndex: 209938`
+  `IsClassic: True`
+  `IsEnabled: True`
+  `LogFile: %SystemRoot%\System32\Winevt\Logs\Windows PowerShell.evtx`
+  `LogType: Administrative`
+  `MaxSizeBytes: 15728640`
+  `MaxBuffers: 64`
+**`System`**
+  `DisplayName: System`
+  `Records: 39157`
+  `FileSize: 20975616`
+  `IsFull: False`
+  `...`
+ +If you need to find a log and know a few characters from its name, you can filter the output by adding the `-l` option. For example, to show only those logs that contain the word `App` we can do this:
+ +`EventLogMonitor.exe -d -l app`
+**`LogName : Entries : LastWriteTime : [DisplayName]`**
+**`-------------------------------------------------`**
+**`Application`** `: 55221 : 23/01/2022 17:39:26 : [Application]`
+**`Microsoft-Windows-Shell-Core/AppDefaults`** `: 720 : 23/01/2022 13:46:39 : [Microsoft-Windows-Shell-Core]`
+**`Microsoft-Windows-Security-LessPrivilegedAppContainer/Operational`** `: 2075 : 23/01/2022 14:47:48 : [Microsoft-Windows-Security-LessPrivilegedAppContainer]`
+**`Microsoft-Windows-AppxPackaging/Operational`** `: 1946 : 23/01/2022 14:32:00 : [Microsoft-Windows-AppxPackagingOM]`
+**`Microsoft-Windows-AppXDeploymentServer/Operational`** `: 4594 : 23/01/2022 15:37:49 : [Microsoft-Windows-AppXDeployment-Server]`
+**`Microsoft-Windows-AppXDeployment/Operational`** `: 2061 : 23/01/2022 17:38:00 : [Microsoft-Windows-AppXDeployment]`
+**`Microsoft-Windows-AppReadiness/Operational`** `: 886 : 23/01/2022 14:32:00 : [Microsoft-Windows-AppReadiness]`
+**`Microsoft-Windows-AppReadiness/Admin`** `: 2529 : 23/01/2022 14:32:00 : [Microsoft-Windows-AppReadiness]`
+**`Microsoft-Windows-AppModel-Runtime/Admin`** `: 1535 : 23/01/2022 17:38:22 : [Microsoft-Windows-AppModel-Runtime]`
+**`Microsoft-Windows-Application-Experience/Program-Telemetry`** `: 1537 : 23/01/2022 14:47:48 : [Microsoft-Windows-Application-Experience]`
+**`Microsoft-Windows-Application-Experience/Program-Compatibility-Assistant`** `: 337 : 23/01/2022 15:47:49 : [Microsoft-Windows-Application-Experience]`
+**`Microsoft-Windows-Application Server-Applications/Operational`** `: 1 : 19/02/2021 16:01:59 : [Microsoft-Windows-Application Server-Applications]`
+ +`Some providers maybe ignored (not Admin).`
+`12 Providers listed.`
+ +Now we can see by specifying `-l app` we have cut down the output from over 141 log providers to just 12. Note that we can specify multiple providers with a comma as in `-l "app, hyper"` or use an `*` to explicitly request all logs. + +## Viewing an exported log file +You can also use the `-l` option to view the events in an exported event log file, rather than an actual event log. For example:
+ +`EventLogMonitor -l c:\temp\WonderApp.evtx -p *`
+`...`
+`5 Entries shown from the c:\temp\WonderApp.evtx log matching the event source '*'.`
+ +Note that when using an event log file, you will need to specify `-p` to see the contents of the log. Also note that tailing is automatically disabled when viewing a file. All the other options such as `-s` along with the other viewing options described below work on event log files. + +To see more examples of using event log files, look at some of the tests for EventLogMonitor which use exported log files extensively to ensure consistent output. + +When using event log files exported from a different machine, it may be necessary to copy the message catalogue `.dll` (or `.exe` in a few cases) file from the source machine to the one being used to read the log file in order to be able to read the events properly. Simply place the `.dll` file into the same folder as the log file itself and give it the same name as the log file, but with a .dll extension. For example for, if you have an exported log in the temp folder called `c:\temp\WonderApp.evtx`, place the message catalogue dll into the `c:\temp` folder and call it `c:\temp\WonderApp.dll` and the EventLogMonitor will use this file when reading the log to display the events. + +Remember, an exported event log file cannot be tailed so you should use the `-p` and `-s` options amongst others to view the events in the log file. + +## The type and shape of events +As documented in these rather old Microsoft MMC [guidelines](https://docs.microsoft.com/en-us/previous-versions/windows/desktop/bb226812(v=vs.85)), there are traditionally three main types of events that can be sent to the Event Log: +* "*Informational* events indicate that a task or operation has been completed successfully." +* "*Warning* events notify the user that a problem might occur unless action is taken." +* "*Error* events provide information about a problem that has occurred with a component or program." + +Each of these event types has the same format as shown in this picture taken from the above guidelines: + +![Event Log message mormat](./images/EventLogExample.png) + +Of this list of six parts, the most frequently used are: +1. Message body (or description). +2. Further message details (or explanation). +3. User action. + +Of course all three of these may include what the picture calls the "List of parameters". "Additional data" and "Standard cross-reference" are rarely seen and are included with the "User action" for our purposes. + +EventLogMonitor always uses the "List of parameters" as part of the message formatting if they are present in the event. + +## Controlling the output +The output from EventLogMonitor is customisable to a certain extent. + +By default the tool tries to show one event per line. It does this by splitting the event into the three main sections shown above and only outputting the first, which is the "Message body". + +There are three options which control the amount of information shown in the output which are are `-1`, `-2` and `-3`. The `-1` option is the default and will only show the "Message body". Specifying `-2` means that the output will include the "Message body" and the "Further details" section whereas specifying `-3` means all parts of the event will be shown including any "Additional data" or "Standard cross-reference" sections and provides the complete event information. + +## Filtering the output +Some applications can produce a large amount of events in the Event Log and given that each application will normally use the same event source name you cannot use the `-s` source option to filter within a single event source and this is where the filter options come in. There are four filter options so far: + +* `-fi` or "filter include". +* `-fx` or "filter exclude". +* `-fw` or "filter on warnings". +* `-fe` or "filter on errors". +* `-fn` or "filter on event number" (coming soon!). + +### "Filter Include" +`-fi` will output only those events that include the specified text in the message. Use quotes to include text that contains a space, for example:
+ +`EventLogMonitor.exe -p * -s -fi "your text here"`
+ +Note that this filter is applied after any `-2` or `-3` option. +### "Filter eXclude" +`-fx` will output only those events that do not include the specified text in the message. Use quotes to exclude text that contains a space, for example:
+ +`EventLogMonitor.exe -p * -s -fx "your text to exclude here"`
+ +Note that this filter is applied after any `-2` or `-3` option. +### "Filter on Warnings" +`-fw` will output only those events that are either a "warning", an "error" or "critical", for example:
+`EventLogMonitor.exe -p * -s -fw`
+ +### "Filter on Errors" +`-fe` will output only those events that are an "error" or "critical", for example:
+ +`EventLogMonitor.exe -p * -s -fe`
+ +If necessary, the `-fi` and `-fe` options can be combined by specifying both options. If both are present, the `-fi` is always run first, then the `-fx` filter is run afterwards. If both `-fw` and `-fe` options are used, the `-fe` option takes precedence. + +Note that when viewing previous events with the `-p` option, the `-fi` and `-fx` options are applied only to those events selected by the `-p`. This means that if you use `-p5` for example then the filter will only be applied to the last 5 events matching the specified `-s` source. Therefore, it is possible no events will be displayed if the filter does not match. To determine if any previous event matches your filter it necessary to use a much larger value for `-p` or even use `-p *` to filter against all previous events in the chosen log. + +## Binary data output +Some events will include diagnostic binary data as part of the event. This will often be ASCII or Unicode text data but may also include pointers or a raw memory dump. To facilitate the viewing of this information, you can use the `-b1` flag to view any binary data contained with an event as ASCII or Unicode text:
+ +`EventLogMonitor.exe -p * -s -b1`
+ +By default the `-b1` option is automatically applied to any error level event that is output. For example:
+ +`EventLogMonitor.exe -s VSS -p 2`
+ +**8224I:** `The VSS service is shutting down due to idle timeout. [19/12/2021 05:04:48.302]`
+**8193E:** `Volume Shadow Copy Service error: Unexpected error calling routine QueryFullProcessImageNameW. hr = 0x8007001f, A device attached to the system is not functioning. [19/12/2021 20:54:45.365]`
+**`- Code: SECSECRC00000581- Call: SECSECRC00000565- PID: 00032976- TID: 00002904- CMD: C:\WINDOWS\system32\vssvc.exe - User: Name: NT AUTHORITY\SYSTEM, SID:S-1-5-18. Index: 277479`**
+ +In the example above the last line in bold shows an example of the automatic application of the `-b1` option for an error event. If the event did not have any binary data, the last line would instead look like this:
+ +`. Index: 315903`
+ +Alternatively if the event had binary data that was not detected as ASCII or Unicode, then the last line would be:
+ +`, Index: 315903`
+ +This message indicates that there is binary data to look at, but you need to use the `-b2` option instead to view it as a hexdump. + +Note that the event index is always shown. + +To view binary data as a hex dump instead of text, use the `-b2` option:
+ +`EventLogMonitor.exe -s Restore -p 1 -b2`
+**8216I:** `Skipping creation of restore point (Process = C:\WINDOWS\winsxs\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.19041.1371_none_7e1bd7147c8285b0\TiWorker.exe -Embedding; Description = Windows Modules Installer) as there is a restore point available which is recent enough for System Restore.` **`[19/01/2022 22:12:10.528]`**
+`Binary Data size: 36`
+`Count : 00 01 02 03-04 05 06 07 ASCII 00 04`
+`00000008: 00 00 00 00-55 02 00 00 ....U... 00000000 00000255`
+`00000016: 4B 02 00 00-00 00 00 00 K....... 0000024B 00000000`
+`00000024: 22 CE 28 67-7c 6d da 79 ".(g|m.y 6728CE22 79DA6D7C`
+`00000032: E2 8C 1C 00-00 00 00 00 ........ 001C8CE2 00000000`
+`00000036: 00 00 00 00 .... 00000000`
+`Index: 311242`
+ +Both binary options (`-b1` and `-b2`) also output the index value for the event which can be used to view more information about the event, for example by adding the `-3` or `-v` options in conjunction with the `-i` option. See the *[Using Event Indexes](#indexes)* section below for more details. + +All data written by the binary options is coloured in blue for easy identification. + +## Verbose output +In addition to the information output choices above, there is another `-v` "verbose" output option that will add some extra information for each event. The `-v` option output will always contain information for: + +* `Machine`: The name of the machine where the event was originally written. This can be useful when viewing an event log file on a different machine. +* `Log`: The name of the log that contains this event. +* `Source`: The name of the source that output this event. This is useful when using `-s *` to see what source each event belongs to. + +In addition, some extra information will be output if it is present in the event, however many events do not contain this information: + +* `User`: The SID for the user which was running the process that output this event. +* `ProcessId`: The ID of the process that output this event. +* `ThreadId:` The ID of the thread within the process that output this event. +* `Version:` The version of the event if the version is greater than zero. +* `Win32Msg:` Output only in certain cases. See the [Viewing events without message catalogues](#no-catalogue) section for more details. + +For example:
+ +`EventLogMonitor.exe -p 1 -s Restart -v`
+**10001I**`: Ending session 0 started 2022 - 01 - 25T00:19:00.327747000Z.` **`[25/01/2022 00:19:13.447]`**
+`Machine: Rivendell. Log: Application. Source: Microsoft-Windows-RestartManager. User: S-1-5-18. ProcessId: 33476. ThreadId: 40936.`
+ +All data written by the `-v` option is coloured in dark grey for easier identification. + +## Using event indexes +Yet another way to use this tool is to query specific events by index with the `-i` or index option. This allows you to output events by index rather than by type. Every event written to an event log has an index number that is put into the event when it is written. Events with higher numbers occurred after events with lower numbers and in the normal case are usually consecutive within a given log. The `-i` option on its own will output the single event with that number, assuming one exists. For example:
+ +`EventLogMonitor.exe -i 123456`
+ +This will output the event with the index `123456` assuming an event with that index exists. You can also specify a range of events to be output by specifying the beginning and end of the range, separated with a `-`. For example:
+ +`EventLogMonitor.exe -i 123456-123460`
+ +This will output the five events, `123456`, `123457`, `123458`, `123459` and `123460` assuming they exist. If events are missing from the range they are simply ignored and specifying an empty range will simply produce no events. When using a range, the end of the range must have a higher value than the beginning of the range. + +We can also specify a single index and use the `-p` option to specify a number of events before and after the indexed event to output as well. For example:
+ +`EventLogMonitor.exe -i 123456 -p 3`
+ +This will output 3 events immediately before the indexed event (if they exist) and 3 events after and is essentially a faster way of specifying a range of events to output. + +This can be especially useful when you are monitoring the event log for an application with `-s` and an error event is emitted. In this case, the event's index will also be output as part of the error event which allows you to take the index and run the tool with a `-p` value, for example `-p 10` and see the ten events immediately before and after the event that had the error. Because the event is specified by index, any `-s` (source) is ignored and events from all sources are displayed. This could allow you to see if another application wrote a message that had a bearing on your application's error. + +Note that when using indexes to access events the "tailing" functionality is automatically disabled, and the command will complete once it has output the specified events. + +## Redirecting the output +The tool supports redirecting the output to a file with the standard shell redirect. For example:
+ +`EventLogMonitor.exe -p 5 -s * > c:\temp\logoutput.txt`
+ +When redirecting output to a file, you can still press ``, `'Q'` or `` to exit or press `'S'` for current stats, although the stats output message will also be redirected to the file. + +## Viewing output in a different language +You can use the `-c ` option to change the culture (or language) used to output the language. Any valid culture is allowed as long as the message catalogue for the event contains the message in the chosen language. Valid values for `-c` include: +* De-DE +* Es-ES +* Fr-FR +* It-IT +* Ja-JP +* Ko-KR +* Pl-PL +* Pt-BR +* Ru-RU +* Tr-TR +* Zh-CN +* Zh-TW + +Note that you may need to use a Unicode font to be able to display certain languages in your terminal. + +## Viewing events without message catalogues +If the message catalogue for an event cannot be found, or the catalogue does not contain an entry for the event in question, a default message is output instead. Normally that message looks similar to the one output by the Event Viewer built into Windows in this situation:
+**0I**`: The description for Event ID 0 from source XYZ cannot be found. Either the component that raises this event is not installed on your local computer or the installation is corrupted. You can install or repair the component on the local computer. [25/01/2020 20:30:25.632]`
+ +However, on occasion, when viewed in the Windows Event Viewer you will actually something like this instead:
+ +`The operation completed successfully.`
+for an event ID of 0. + +or this: + +`Incorrect function.`
+for an event ID of 1. + +or even: + +`The system cannot find the path specified.`
+for an event ID of 3. + +What is happening here is that if the Event Viewer detects that the event was written with a "qualifier" of zero (see [EventRecord.Qualifiers](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.eventing.reader.eventrecord.qualifiers?view=dotnet-plat-ext-6.0#system-diagnostics-eventing-reader-eventrecord-qualifiers)) it tries to convert the event ID into a Win32 error message. If that convertion is sucessful then the Win32 error message that corresponds to the event ID is output instead of the default error message shown above. Whilst this approach means the event viewer output contains fewer error messages like the one above, it can be misleading in many cases as the Win32 message may not match the event. Therefore, EventLogMonitor chooses to always output the original error message instead which more acurately reflects the situation. However, if you also use the `-v` "verbose" option then you will see an extra entry on the verbose output line for the `Win32Msg` in this case:
+ +`Machine: mgk-PC3. Log: Application. Source: Firefox Default Browser Agent. Win32Msg: The operation completed successfully. (0).`
+ +or perhaps: + +`Machine: mgk-PC3. Log: Application. Source: iBtSiva. Win32Msg: The system cannot find the path specified. (3).`
+ +Of coourse the exact message shown will reflect the actual event ID. This allows you to see the same information in EventLogMonitor that you do in the Event Viewer. + +## Viewing the Security log +The `Security` log can be viewed like any other log by specifing it's name with the `-l` option:
+ +`EventLogMonitor.exe -l Security`
+ +However, you must run this command from an elevated command prompt or you will get an error:
+ +`Attempted to perform an unauthorized operation.`
+`Run from an elevated command prompt to access the 'Security' event log.`
+ +Once your prompt is elevated then all the other options like `-p` and `-3` etc, work just the same against the `Security` log. The main difference between the `Security` and other logs is that rather than using `Information`, `Warning` and `Error` for the categories of events, it uses `Audit Success` and `Audit Failure`. These are represented as follows: + +* **Audit Success** event numbers are written in green. They are also suffixed with the letter `S` for easy identification. +* **Audit Failure** event numbers are written in red. They are also suffixed with the letter `F` for easy identification.
+ +## Miscellaneous options +There are a final few options that have not been covered elsewhere. These are: +* `-nt` or "No Tailing". If you are only wanting to view existing events, specifying `-nt` will stop the tool tailing the log at the end of the output. +* `-?` or `-help`. The help commands produce a simplified version of this readme. +* `-version`. Displays the version of the EventLogMonitor tool being run. + +## Options list +To see all the options, ask for help:
+`EventLogMonitor -?`
+ +Note that all the options also support a `/` as well as a `-`:
+`EventLogMonitor /?`
+## IBM events +If you run the tool without any options at all, you will see that the default is to look for entries from the various names for the **IBM App Connect Enterprise** product:
+ +`EventLogMonitor`
+`Waiting for events from the Application log matching the event source 'IBM Integration' or 'WebSphere Broker' or 'IBM App Connect Enterprise'.`
+`Press , 'Q' or to exit or press 'S' for current stats...`
+ +As you can see it looks for the most recent three names by which the product's event log entries have been known. One other small change the tool makes is when it outputs an entry that belongs to one of these products it will prefix the name with the letters `BIP` to match the products message naming convention. + +However, if you are not using this tool with any of these products, simply override these defaults with the `-s` flag as described above in the [usage](#usage) section. + +## License +The source code files are made available under the Apache License, Version 2.0 (Apache-2.0), located in the [LICENSE](https://github.com/m-g-k/EventLogMonitor/blob/main/LICENSE) file. + +## Questions, suggestions and problems +Please create any [issues and suggestions on GitHub](https://github.com/m-g-k/EventLogMonitor/issues). \ No newline at end of file diff --git a/images/EventLogExample.png b/images/EventLogExample.png new file mode 100644 index 0000000..2c0f864 Binary files /dev/null and b/images/EventLogExample.png differ diff --git a/src/EventLogMonitor/BinaryDataFormatter.cs b/src/EventLogMonitor/BinaryDataFormatter.cs new file mode 100644 index 0000000..0552cea --- /dev/null +++ b/src/EventLogMonitor/BinaryDataFormatter.cs @@ -0,0 +1,281 @@ +/* + Copyright 2012-2022, MGK + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Text; + +namespace EventLogMonitor; + +public static class BinaryDataFormatter +{ + // convertor that throws on errors + readonly private static UnicodeEncoding iConvertor = new(false, false, true); + readonly private static Char[] iTrimChars = new Char[] { ' ', '\n', '\t', '\r' }; + + public static bool OutputFormattedBinaryDataAsString(byte[] data, long index) + { + if (data == null || data.Length == 0) + { + return OutputNoDataError(index); + } + + // quick and dirty test for ascii only string data or possible binary only data + bool isAscii = true; + bool isBinary = false; + foreach (byte x in data) + { + if (x is < 0x20 or > 0x7F) + { + if (x is < 0x20 and not 0x0) + { + // if we are probably binary we can leave early + isBinary = true; + isAscii = false; + break; + } + // we can't break here as we could find a NULL or a char >0x7F + // before a byte <0x20 so we keep going. + isAscii = false; + } + } + + string message; + if (isAscii) + { + message = Encoding.ASCII.GetString(data); + } + else if (isBinary) + { + message = ""; + } + else + { + // The default static Unicode object is not configured to throw on invalid + // unicode characters so we use our own. + try + { + message = iConvertor.GetString(data); + } + catch (ArgumentException) + { + // not valid unicode + message = ""; + } + } + + message = message.Replace('\0', ' '); // strip embedded nulls + message = message.TrimEnd(iTrimChars); // remove junk + Console.ForegroundColor = ConsoleColor.DarkCyan; + Console.WriteLine(message + ". Index: " + index); + Console.ResetColor(); + + return true; + } + + public static bool OutputFormattedBinaryData(byte[] data, long index) + { + if (data == null || data.Length == 0) + { + return OutputNoDataError(index); + } + + int counter1; + int counter2; + int dataSize = data.Length; + int lineCount = dataSize / 8; + int lineFraction = dataSize % 8; + + StringBuilder buffer = new(); + + Console.Write("Binary Data size: {0}\n" + + "Count : 00 01 02 03-04 05 06 07 ASCII 00 04\n", dataSize); + + // first print whole lines + for (counter1 = 0; counter1 < lineCount; ++counter1) + { + counter2 = counter1 * 8; + buffer.AppendFormat( + "{0:D8}: {1:X2} {2:X2} {3:X2} {4:x2}-{5:x2} {6:x2} {7:x2} {8:x2} {9}{10}{11}{12}{13}{14}{15}{16} {17:X2}{18:X2}{19:X2}{20:X2} {21:X2}{22:X2}{23:X2}{24:X2}\n", + counter2 + 8, + data[counter2 + 0], + data[counter2 + 1], + data[counter2 + 2], + data[counter2 + 3], + data[counter2 + 4], + data[counter2 + 5], + data[counter2 + 6], + data[counter2 + 7], + IsPrintable(data[counter2 + 0]) ? (char)data[counter2 + 0] : '.', + IsPrintable(data[counter2 + 1]) ? (char)data[counter2 + 1] : '.', + IsPrintable(data[counter2 + 2]) ? (char)data[counter2 + 2] : '.', + IsPrintable(data[counter2 + 3]) ? (char)data[counter2 + 3] : '.', + IsPrintable(data[counter2 + 4]) ? (char)data[counter2 + 4] : '.', + IsPrintable(data[counter2 + 5]) ? (char)data[counter2 + 5] : '.', + IsPrintable(data[counter2 + 6]) ? (char)data[counter2 + 6] : '.', + IsPrintable(data[counter2 + 7]) ? (char)data[counter2 + 7] : '.', + data[counter2 + 3], + data[counter2 + 2], + data[counter2 + 1], + data[counter2 + 0], + data[counter2 + 7], + data[counter2 + 6], + data[counter2 + 5], + data[counter2 + 4]); + } + + // now do any fractions + if (lineFraction == 7) + { + counter2 = counter1 * 8; + buffer.AppendFormat( + "{0:D8}: {1:X2} {2:X2} {3:X2} {4:x2}-{5:x2} {6:x2} {7:x2} {8}{9}{10}{11}{12}{13}{14} {15:X2}{16:X2}{17:X2}{18:X2}\n", + counter2 + 7, + data[counter2 + 0], + data[counter2 + 1], + data[counter2 + 2], + data[counter2 + 3], + data[counter2 + 4], + data[counter2 + 5], + data[counter2 + 6], + IsPrintable(data[counter2 + 0]) ? (char)data[counter2 + 0] : '.', + IsPrintable(data[counter2 + 1]) ? (char)data[counter2 + 1] : '.', + IsPrintable(data[counter2 + 2]) ? (char)data[counter2 + 2] : '.', + IsPrintable(data[counter2 + 3]) ? (char)data[counter2 + 3] : '.', + IsPrintable(data[counter2 + 4]) ? (char)data[counter2 + 4] : '.', + IsPrintable(data[counter2 + 5]) ? (char)data[counter2 + 5] : '.', + IsPrintable(data[counter2 + 6]) ? (char)data[counter2 + 6] : '.', + data[counter2 + 3], + data[counter2 + 2], + data[counter2 + 1], + data[counter2 + 0]); + } + else if (lineFraction == 6) + { + counter2 = counter1 * 8; + buffer.AppendFormat( + "{0:D8}: {1:X2} {2:X2} {3:X2} {4:x2}-{5:x2} {6:x2} {7}{8}{9}{10}{11}{12} {13:X2}{14:X2}{15:X2}{16:X2}\n", + counter2 + 6, + data[counter2 + 0], + data[counter2 + 1], + data[counter2 + 2], + data[counter2 + 3], + data[counter2 + 4], + data[counter2 + 5], + IsPrintable(data[counter2 + 0]) ? (char)data[counter2 + 0] : '.', + IsPrintable(data[counter2 + 1]) ? (char)data[counter2 + 1] : '.', + IsPrintable(data[counter2 + 2]) ? (char)data[counter2 + 2] : '.', + IsPrintable(data[counter2 + 3]) ? (char)data[counter2 + 3] : '.', + IsPrintable(data[counter2 + 4]) ? (char)data[counter2 + 4] : '.', + IsPrintable(data[counter2 + 5]) ? (char)data[counter2 + 5] : '.', + data[counter2 + 3], + data[counter2 + 2], + data[counter2 + 1], + data[counter2 + 0]); + } + else if (lineFraction == 5) + { + counter2 = counter1 * 8; + buffer.AppendFormat( + "{0:D8}: {1:X2} {2:X2} {3:X2} {4:x2}-{5:x2} {6}{7}{8}{9}{10} {11:X2}{12:X2}{13:X2}{14:X2}\n", + counter2 + 5, + data[counter2 + 0], + data[counter2 + 1], + data[counter2 + 2], + data[counter2 + 3], + data[counter2 + 4], + IsPrintable(data[counter2 + 0]) ? (char)data[counter2 + 0] : '.', + IsPrintable(data[counter2 + 1]) ? (char)data[counter2 + 1] : '.', + IsPrintable(data[counter2 + 2]) ? (char)data[counter2 + 2] : '.', + IsPrintable(data[counter2 + 3]) ? (char)data[counter2 + 3] : '.', + IsPrintable(data[counter2 + 4]) ? (char)data[counter2 + 4] : '.', + data[counter2 + 3], + data[counter2 + 2], + data[counter2 + 1], + data[counter2 + 0]); + } + else if (lineFraction == 4) + { + counter2 = counter1 * 8; + buffer.AppendFormat( + "{0:D8}: {1:X2} {2:X2} {3:X2} {4:x2} {5}{6}{7}{8} {9:X2}{10:X2}{11:X2}{12:X2}\n", + counter2 + 4, + data[counter2 + 0], + data[counter2 + 1], + data[counter2 + 2], + data[counter2 + 3], + IsPrintable(data[counter2 + 0]) ? (char)data[counter2 + 0] : '.', + IsPrintable(data[counter2 + 1]) ? (char)data[counter2 + 1] : '.', + IsPrintable(data[counter2 + 2]) ? (char)data[counter2 + 2] : '.', + IsPrintable(data[counter2 + 3]) ? (char)data[counter2 + 3] : '.', + data[counter2 + 3], + data[counter2 + 2], + data[counter2 + 1], + data[counter2 + 0]); + } + else if (lineFraction == 3) + { + counter2 = counter1 * 8; + buffer.AppendFormat( + "{0:D8}: {1:X2} {2:X2} {3:X2} {4}{5}{6}\n", + counter2 + 3, + data[counter2 + 0], + data[counter2 + 1], + data[counter2 + 2], + IsPrintable(data[counter2 + 0]) ? (char)data[counter2 + 0] : '.', + IsPrintable(data[counter2 + 1]) ? (char)data[counter2 + 1] : '.', + IsPrintable(data[counter2 + 2]) ? (char)data[counter2 + 2] : '.'); + } + else if (lineFraction == 2) + { + counter2 = counter1 * 8; + buffer.AppendFormat( + "{0:D8}: {1:X2} {2:X2} {3}{4}\n", + counter2 + 2, + data[counter2 + 0], + data[counter2 + 1], + IsPrintable(data[counter2 + 0]) ? (char)data[counter2 + 0] : '.', + IsPrintable(data[counter2 + 1]) ? (char)data[counter2 + 1] : '.'); + } + else if (lineFraction == 1) + { + counter2 = counter1 * 8; + buffer.AppendFormat( + "{0:D8}: {1:X2} {2}\n", + counter2 + 1, + data[counter2 + 0], + IsPrintable(data[counter2 + 0]) ? (char)data[counter2 + 0] : '.'); + } + Console.ForegroundColor = ConsoleColor.DarkCyan; + Console.Write(buffer.ToString()); + Console.WriteLine("Index: " + index); + Console.ResetColor(); + return true; + } + + private static bool IsPrintable(byte candidate) + { + return candidate is not (< 0x20 or > 0x7F); + } + + private static bool OutputNoDataError(long index) + { + Console.ForegroundColor = ConsoleColor.DarkCyan; + Console.WriteLine(". Index: " + index); + Console.ResetColor(); + return false; + } + +} \ No newline at end of file diff --git a/src/EventLogMonitor/CultureSpecificMessage.cs b/src/EventLogMonitor/CultureSpecificMessage.cs new file mode 100644 index 0000000..282f442 --- /dev/null +++ b/src/EventLogMonitor/CultureSpecificMessage.cs @@ -0,0 +1,289 @@ +/* + Copyright 2012-2022, MGK + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Diagnostics.Eventing.Reader; +using System.Collections.Generic; +using Microsoft.Win32; + + +namespace EventLogMonitor; +public static class CultureSpecificMessage +{ + private const int LOAD_LIBRARY_AS_DATAFILE = 0x00000002; + private const int LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "LoadLibraryExW", SetLastError = true, ExactSpelling = true)] + private static extern IntPtr LoadLibraryEx(string libFilename, IntPtr reserved, int flags); + + [DllImport("kernel32.dll")] + private static extern bool FreeLibrary(IntPtr hModule); + + private const int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; + private const int FORMAT_MESSAGE_FROM_HMODULE = 0x00000800; + private const int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + private const int FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000; + private const int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100; + private const int FORMAT_MESSAGE_MAX_WIDTH_MASK = 0x000000FF; + private const int USEnglishLCID = 1033; + readonly private static Dictionary iCatalogueCache = new(); + + [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "FormatMessageW", SetLastError = true, BestFitMapping = true, ExactSpelling = true)] + private static extern int FormatMessageW( + int dwFlags, + IntPtr lpSource, + uint dwMessageId, + int dwLanguageId, + ref IntPtr lpBuffer, + int nSize, + string[] pArguments); + + public static string GetCultureSpecificMessage(IEventLogRecordWrapper entry, int cultureLCID) + { + // make a list of inserts, but ignore the last entry if this is binary + int insertCount = entry.Properties.Count; + List insertList = new(insertCount); + for (int i = 0; i < (insertCount); ++i) + { + object insert = entry.Properties[i].Value; + if (insert is byte[]) + { + // use an empty string or we get the string "System.Byte[]" not the value. + // the user can see the value if they provide -b1 or -b2 as byte[]'s are always the last insert + insertList.Add(""); + } + else + { + insertList.Add(insert.ToString()); + } + } + + string provider = entry.ProviderName; // e.g "IBM App Connect Enterprise v110011" + string logName = entry.LogName; // e.g. "Application" + string providerRegPath = @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\" + logName + @"\" + provider; + string catalogueLocation = (string)Registry.GetValue(providerRegPath, "EventMessageFile", null); + + if (string.IsNullOrEmpty(catalogueLocation)) + { + if (EventLogMonitor.LogIsAFile(entry.ContainerLog)) + { + // see if we have a message dll to use next to the current file + int index = entry.ContainerLog.LastIndexOf('\\'); + string logBaseName = entry.ContainerLog[(index + 1)..]; + string logBaseLocation = entry.ContainerLog[..(index + 1)]; + index = logBaseName.LastIndexOf('.'); + string fileName = logBaseName[..index]; + fileName += ".dll"; + string fileFullName = logBaseLocation + fileName; + if (File.Exists(fileFullName)) + { + catalogueLocation = fileFullName; + } + } + + // check again + if (string.IsNullOrEmpty(catalogueLocation)) + { + // we don't have a message catalogue DLL so can't get a culture specific message + return string.Empty; + } + } + else + { + if (catalogueLocation.Contains(';')) + { + // some registry entries, esp' for device drivers, have multiple options, which are semicolon separated. + // for now just pick the first, but we will probably have to try each one in a loop at some point + // however, picking the first seems to work ok for now + string[] paths = catalogueLocation.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (paths.Length >= 1) + { + catalogueLocation = paths[0]; + } + } + } + + IntPtr dllHandle; + if (iCatalogueCache.ContainsKey(catalogueLocation)) + { + dllHandle = iCatalogueCache[catalogueLocation]; + } + else + { + int flags = LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE; + dllHandle = LoadLibraryEx(catalogueLocation, IntPtr.Zero, flags); + if (dllHandle != IntPtr.Zero) + { + iCatalogueCache[catalogueLocation] = dllHandle; + } + else + { + // we can't load the message catalogue DLL so can't get a culture specific message + return string.Empty; + } + } + + // The native FormatMessage expects the qualifier as the high word of the msg number + int finalCode; + if (entry.Qualifiers > 0) + { + finalCode = (int)entry.Qualifiers << 16; + finalCode += entry.Id; + } + else + { + finalCode = entry.Id; + } + + string responseMsg = GetMessage(finalCode, insertList.ToArray(), cultureLCID, dllHandle); + return responseMsg; + } + static string GetMessage(int msgCode, string[] arguments, int cultureLCID, IntPtr moduleHandle) + { + //int flags = FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER; + int flags = FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_ALLOCATE_BUFFER; + + if (arguments.Length > 0) + { + flags |= FORMAT_MESSAGE_ARGUMENT_ARRAY; + } + else + { + flags |= FORMAT_MESSAGE_IGNORE_INSERTS; + } + + // we need to set the low order byte to stop FormatMessage duplicating line breaks in the output message. If not set + // all '\r\n' (%n) sequences come out as '\r\n\r\n' which is a real pain and messes up the '-2' (medium output) option. + // see https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-formatmessage for more details. + flags += FORMAT_MESSAGE_MAX_WIDTH_MASK; + + // for faster unsafe method see: https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FormatMessage.cs + IntPtr nativeBuffer = IntPtr.Zero; + try + { + int currentCulture = cultureLCID; + while (true) + { + int length = FormatMessageW(flags, moduleHandle, unchecked((uint)msgCode), currentCulture, ref nativeBuffer, 65535, arguments); + // Console.WriteLine("Len: " + length + ", lastErr: " + Marshal.GetLastWin32Error()); // debugging + + if (length > 0) + { + string formattedString = Marshal.PtrToStringUni(nativeBuffer, length); + return formattedString; + } + else + { + int lastError = Marshal.GetLastWin32Error(); + if (lastError == 1815 || (lastError >= 15100 && lastError <= 15108) || lastError == 317) + { + // Code definitions nelow are from here: https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes + + // Error code '1815' is "ERROR_RESOURCE_LANG_NOT_FOUND" which means: + // "The specified resource language ID cannot be found in the image file." + + // The range 15100 - 15108 are all MUI errors which generally mean the language + // resource dll is not found or is corrupt or the lang is not found in the dll + // for example errors seen in testing include: + + // Error code '15105' is "ERROR_MUI_FILE_NOT_LOADED" which means: + // "The resource loader cache doesn't have loaded MUI entry." + // AKA - Language not found in DLL + + // Error code '15100' is "ERROR_MUI_FILE_NOT_FOUND" which means: + // "The resource loader failed to find MUI file." + // AKA language resource dll not found + + // Error code 317 is "ERROR_MR_MID_NOT_FOUND" which means: + // The system cannot find message text for message number 0x%1 in the message file for %2. + // AKA - yet another message not found in dll error code! + + // try again with US English if we have not already. + if (currentCulture != USEnglishLCID && currentCulture != 0) + { + currentCulture = USEnglishLCID; + continue; + } + + if (currentCulture != 0) + { + // final attempt to find a message + currentCulture = 0; + continue; + } + + // So we return an empty string below to force the use of the default culture + // instead in these cases + } + else + { + Console.WriteLine("Error: " + lastError + " using specified culture."); + } + } + break; + } + } + finally + { + // Free the buffer. + Marshal.FreeHGlobal(nativeBuffer); + } + + // default return to force default culture use + return string.Empty; + } + + // Interface to allow mocking of an EventLogRecord + public interface IEventLogRecordWrapper + { + public string ContainerLog { get; } + public int? Qualifiers { get; } + public int Id { get; } + public string ProviderName { get; } + public IList Properties { get; } + public string LogName { get; } + } + + // Class to allow mocking of an EventLogRecord + public class EventLogRecordWrapper : IEventLogRecordWrapper + { + private readonly EventRecord iLogRecord; + public EventLogRecordWrapper(EventRecord logRecord) + { + iLogRecord = logRecord; + } + public string ContainerLog + { + get + { + return iLogRecord is EventLogRecord record ? record.ContainerLog : null; + } + } + + public int? Qualifiers => iLogRecord.Qualifiers; + + public int Id => iLogRecord.Id; + + public string ProviderName => iLogRecord.ProviderName; + + public IList Properties => iLogRecord.Properties; + + public string LogName => iLogRecord.LogName; + } + +} diff --git a/src/EventLogMonitor/EventLogMonitor.csproj b/src/EventLogMonitor/EventLogMonitor.csproj new file mode 100644 index 0000000..fc9fc5c --- /dev/null +++ b/src/EventLogMonitor/EventLogMonitor.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0-windows + 2.0.0.0 + + + + + + + + diff --git a/src/EventLogMonitor/Program.cs b/src/EventLogMonitor/Program.cs new file mode 100644 index 0000000..817a6e2 --- /dev/null +++ b/src/EventLogMonitor/Program.cs @@ -0,0 +1,1351 @@ +/* + Copyright 2012-2022, MGK + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security; +using System.ComponentModel; + +namespace EventLogMonitor; +public class EventLogMonitor +{ + private static volatile int s_entriesDisplayed = 0; + public EventLogMonitor() + { + + } + + private bool ParseArguments(SimpleArgumentProcessor myArgs) + { + // For Usage see help + myArgs.SetOptionalFlaggedArgument("-p"); + myArgs.SetOptionalBooleanArgument("-1"); + myArgs.SetOptionalBooleanArgument("-2"); + myArgs.SetOptionalBooleanArgument("-3"); + myArgs.SetOptionalBooleanArgument("-v"); + myArgs.SetOptionalBooleanArgument("-b1"); + myArgs.SetOptionalBooleanArgument("-b2"); + myArgs.SetOptionalBooleanArgument("-nt"); + myArgs.SetOptionalBooleanArgument("-tf"); + myArgs.SetOptionalBooleanArgument("-d"); + myArgs.SetOptionalFlaggedArgument("-i"); + myArgs.SetOptionalFlaggedArgument("-s"); + myArgs.SetOptionalFlaggedArgument("-c"); + myArgs.SetOptionalFlaggedArgument("-l"); + myArgs.SetOptionalFlaggedArgument("-fi"); + myArgs.SetOptionalFlaggedArgument("-fx"); + myArgs.SetOptionalBooleanArgument("-fw"); + myArgs.SetOptionalBooleanArgument("-fe"); + + myArgs.SetOptionalBooleanArgument("-?"); // help + myArgs.SetOptionalBooleanArgument("-help"); // help + myArgs.SetOptionalBooleanArgument("-version"); // version + + bool validArgs = myArgs.ValidateArguments(); + if (!validArgs) + { + return InvalidArguments(null); + } + + if (myArgs.GetBooleanArgument("-?") || myArgs.GetBooleanArgument("-help")) + { + DisplayHelp(); + return false; + } + + if (myArgs.GetBooleanArgument("-version")) + { + DisplayVersion(); + return false; + } + + string record = myArgs.GetFlaggedArgument("-p"); + if (record == "*") + { + iPreviousRecordCount = uint.MaxValue; + } + else + { + // ignore parse failure as default will be 0 which is fine + _ = uint.TryParse(record, out iPreviousRecordCount); + } + + string index = myArgs.GetFlaggedArgument("-i"); + bool indexSet = false; + + if (!string.IsNullOrEmpty(index)) + { + indexSet = true; + if (index.Contains('-')) + { + char[] match = { '-' }; + string[] range = index.Split(match, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (range.Length != 2) + { + return InvalidArguments("invalid range, use 'x-y' to specify a range"); + } + else + { + // ignore parse failure as default will be 0 + _ = uint.TryParse(range[0], out iRecordIndexMin); + _ = uint.TryParse(range[1], out iRecordIndexMax); + + if (iRecordIndexMax < iRecordIndexMin) + { + return InvalidArguments("index max > index min"); + } + iOriginalIndex = iRecordIndexMin; + + // set up the indexes to output a range of events. + // they may not all exist but we try to incude them + iRecordIndexRange = iRecordIndexMax - iRecordIndexMin + 1; + + if (iPreviousRecordCount < iRecordIndexRange) + { + iPreviousRecordCount = iRecordIndexRange; + } + } + } + else + { + // ignore parse failure as default will be 0 + _ = uint.TryParse(index, out iRecordIndexMin); + iOriginalIndex = iRecordIndexMin; + if (iPreviousRecordCount > 0) + { + if (iPreviousRecordCount == uint.MaxValue) + { + // set up the indexes to output all events after and including the index event + iRecordIndexMax = iPreviousRecordCount; + iRecordIndexRange = iPreviousRecordCount; + } + else + { + // set up the indexes to output -p events before and after the index + // note those events may not always exist but we try to include them + iRecordIndexMax = iRecordIndexMin + iPreviousRecordCount; + if (iPreviousRecordCount >= iRecordIndexMin) + { + //make sure we don't wrap + iRecordIndexMin = 1; + } + else + { + iRecordIndexMin -= iPreviousRecordCount; + } + iRecordIndexRange = (iPreviousRecordCount * 2) + 1; + } + } + else + { + // set up the indexes to output a single event + iRecordIndexMax = iRecordIndexMin; + iRecordIndexRange = 1; + } + + } + } + + int count = 0; + iVerboseOutput = myArgs.GetBooleanArgument("-v"); + iMinimalOutput = myArgs.GetBooleanArgument("-1"); + iMediumOutput = myArgs.GetBooleanArgument("-2"); + iFullOutput = myArgs.GetBooleanArgument("-3"); + bool doNotTail = myArgs.GetBooleanArgument("-nt"); + if (doNotTail) + { + iTailEventLog = false; + } + + bool tsFirst = myArgs.GetBooleanArgument("-tf"); + if (tsFirst) + { + iTimestampFirst = true; + } + + iDisplayLogs = myArgs.GetBooleanArgument("-d"); + + string filter = myArgs.GetFlaggedArgument("-fi"); // filter include + if (!string.IsNullOrEmpty(filter)) + { + iEntryInclusiveFilter = filter; + } + + filter = myArgs.GetFlaggedArgument("-fx"); // filter exclude + if (!string.IsNullOrEmpty(filter)) + { + iEntryExclusiveFilter = filter; + } + + bool level = myArgs.GetBooleanArgument("-fw"); // filter to only show warnings and above + if (level) + { + iLogLevel = 3; // This level equates to warning events + } + + level = myArgs.GetBooleanArgument("-fe"); // filter to only show errors - overrides an fw + if (level) + { + iLogLevel = 2; // This level equates to error events (will also catch critical events) + } + + string logName = myArgs.GetFlaggedArgument("-l"); + if (!string.IsNullOrEmpty(logName)) + { + iLogName = logName; + } + else + { + if (iDisplayLogs) + { + iLogName = ""; // force empty to allow -l and -i to be specified + } + } + + string source = myArgs.GetFlaggedArgument("-s"); + if (!string.IsNullOrEmpty(source)) + { + if (indexSet) + { + return InvalidArguments("-s not allowed with -i"); + } + + iSource = source; + if (iSource.Contains(',')) + { + char[] match = { ',' }; + iMultiMatch = iSource.Split(match, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + else + { + iMultiMatch = Array.Empty(); // clear the default + } + + } + else + { + // as the user has not specified a source, if the log name is not "Application" we should + // not default to looking for IIB entries only + if (!iLogName.Equals("Application")) + { + iSource = "*"; //look for all entries + iMultiMatch = Array.Empty(); + } + } + + // make the default one first + iUSDefaultCulture = new CultureInfo("En-US"); + string culture = myArgs.GetFlaggedArgument("-c"); + if (!string.IsNullOrEmpty(culture)) + { + iDefaultCulture = culture; + iCultureSet = true; + } + + iMinBinaryOutput = myArgs.GetBooleanArgument("-b1"); + iFullBinaryOutput = myArgs.GetBooleanArgument("-b2"); + + if (iMinimalOutput) + { + ++count; + } + + if (iMediumOutput) + { + ++count; + } + + if (iFullOutput) + { + ++count; + } + + if (count > 1) + { + return InvalidArguments("only one of options '-1', '-2' and '-3' may be specified"); + } + + if (iCultureSet) + { + try + { + iChosenCulture = CultureInfo.CreateSpecificCulture(iDefaultCulture); + } + catch (ArgumentException) + { + // in .NET 4.0, this can throw a CultureNotFoundException, a subclass of ArgumentException! + Console.WriteLine("Culture is not supported. " + iDefaultCulture + " is an invalid culture identifier. Defaulting to 'En-US'."); + iChosenCulture = iUSDefaultCulture; + } + } + return true; + } + + private static bool InvalidArguments(string extraInfo) + { + if (!string.IsNullOrEmpty(extraInfo)) + { + Console.WriteLine("Invalid arguments or invalid argument combination: {0}.", extraInfo); + } + else + { + Console.WriteLine("Invalid arguments or invalid argument combination."); + } + DisplayHelp(); + return false; + } + + private void DisplayAvailableLogs() + { + int providerCount = 0; + EventLog[] logsa = EventLog.GetEventLogs(); + Dictionary oldDisplayNames = new(); + foreach (EventLog log in logsa) + { + try + { + oldDisplayNames.Add(log.Log, log.LogDisplayName); + } + catch (SecurityException) + { + // we will always fail to access the Security log if we are not admin + } + } + + EventLogSession session = EventLogSession.GlobalSession; + string[] logsToMatch; + bool localFile = false; + bool matchAll = false; + PathType pathType = PathType.LogName; + var allLogs = session.GetLogNames().ToArray(); + if (LogIsAFile(iLogName)) + { + pathType = PathType.FilePath; + logsToMatch = new string[] { iLogName }; + localFile = true; + allLogs = logsToMatch; // replace with the file name + } + else if (!string.IsNullOrEmpty(iLogName) && (!iLogName.Equals("*"))) + { + if (iLogName.Contains(',')) + { + char[] match = { ',' }; + logsToMatch = iLogName.Split(match, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + else + { + // assume a single real log name or a partial log name, not a file. + logsToMatch = new string[] { iLogName }; + } + } + else + { + matchAll = true; + logsToMatch = Array.Empty(); + } + + if (!iVerboseOutput) + { + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine("LogName : Entries : LastWriteTime : [DisplayName]"); + Console.WriteLine("-------------------------------------------------"); + Console.ResetColor(); + } + + int ignoreCount = 0; + foreach (string current in allLogs) + { + try + { + EventLogInformation logInfo = session.GetLogInformation(current, pathType); + if (!LogNameMatch(current, logsToMatch, matchAll)) + { + continue; + } + + EventLogConfiguration logConfig = null; + if (!localFile) + { + logConfig = new EventLogConfiguration(current, session); + } + + string displayName = ""; + if (!localFile) + { + if (logConfig.IsClassicLog) + { + if (oldDisplayNames.ContainsKey(current)) + { + displayName = oldDisplayNames[current]; + } + } + else + { + displayName = logConfig.OwningProviderName; + } + } + else + { + displayName = System.IO.Path.GetFileNameWithoutExtension(current); + } + + long recordCountNull = logInfo.RecordCount ?? -1; + if (!iVerboseOutput) + { + // only output logs that have records that can be read. + if (recordCountNull > 0) + { + Console.ForegroundColor = ConsoleColor.White; + Console.Write(current); + Console.ResetColor(); + Console.WriteLine(" : " + logInfo.RecordCount + " : " + logInfo.LastWriteTime + " : [" + displayName + "]"); + ++providerCount; + } + } + else + { + // include more records than above as we count ones with 0 records + if (recordCountNull >= 0) + { + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(current); + Console.ResetColor(); + Console.WriteLine(" DisplayName: " + displayName); + Console.WriteLine(" Records: " + logInfo.RecordCount); + Console.WriteLine(" FileSize: " + logInfo.FileSize); + Console.WriteLine(" IsFull: " + logInfo.IsLogFull); + Console.WriteLine(" CreationTime: " + logInfo.CreationTime); + Console.WriteLine(" LastWrite: " + logInfo.LastWriteTime); + Console.WriteLine(" OldestIndex: " + logInfo.OldestRecordNumber); + if (!localFile) + { + Console.WriteLine(" IsClassic: " + logConfig.IsClassicLog); + Console.WriteLine(" IsEnabled: " + logConfig.IsEnabled); + Console.WriteLine(" LogFile: " + logConfig.LogFilePath); + Console.WriteLine(" LogType: " + logConfig.LogType); + Console.WriteLine(" MaxSizeBytes: " + logConfig.MaximumSizeInBytes); + Console.WriteLine(" MaxBuffers: " + logConfig.ProviderMaximumNumberOfBuffers); + } + ++providerCount; + } + + } + + } + catch (UnauthorizedAccessException) + { + // we will always fail to access the Security log (and others) if we are not admin + // Console.WriteLine(" Ignoring log: " + current + " (not Admin)"); //debug + if (LogNameMatch(current, logsToMatch, matchAll)) + { + ++ignoreCount; + } + } + catch (EventLogException) + { + // Currently only "Microsoft-Windows-UAC/Operational : 1 : 07/07/2014 09:44:36 : [Microsoft-Windows-UAC]" on Windows 8.1 and + // "Microsoft-Windows-USBVideo/Analytic" on Windows 10 have this exception so ignore for now... + // Console.WriteLine(" Exception finding log: " + current + " : " + e.ToString()); + } + } + + Console.WriteLine(); + if (ignoreCount > 0) + { + // Although we know how many were skipped, we don't know how many of these have 0 entries and not be shown anyway, + // so we choose not to put out the count here as it gets confusing if you rerun with Admin and the totals don't match. + Console.WriteLine("Some providers maybe ignored (not Admin)."); + } + + if (localFile) + { + Console.WriteLine(providerCount + " Provider file listed."); + } + else + { + Console.WriteLine(providerCount + " Providers listed."); + } + + } + + private void DisplayMatchingEvents() + { + //see if the user has given us a path to a file + PathType pathType = PathType.LogName; + if (LogIsAFile(iLogName)) + { + iTailEventLog = false; //cannot tail a file + pathType = PathType.FilePath; + } + + string query = null; // no query by default + if (iLogLevel != -1) + { + // LogAlways = 0, + // Critical = 1, + // Error = 2, + // Warning = 3, + // Informational = 4, + // Verbose = 5 + // Customer events = > 5 + query = $"*[System/Level<={iLogLevel} and System/Level>0]"; + } + + iEventLogQuery = new EventLogQuery(iLogName, pathType, query) + { + ReverseDirection = true //we want newest to oldest + }; + + using var reader = new EventLogReader(iEventLogQuery); + + //display existing entries before we move onto waiting for new entries to appear + List entries = new(); + int matched = 0; + if (iRecordIndexMin > 0) + { + // we are looking to output either a single event, a complete range (-i xxx-yyy) or a number before and after the index chosen (-i zzz -p x) + // where we have x events before and after the event (assuming events with those indexes exist). + for (EventRecord current = reader.ReadEvent(); (matched <= iRecordIndexRange) && (current != null); current = reader.ReadEvent()) + { + if (current.RecordId >= iRecordIndexMin && current.RecordId <= iRecordIndexMax) + { + ++matched; + entries.Add(current); + } + } + + // output matched entries in event log order + OutputEntriesInEventLogOrder(entries); + + if (s_entriesDisplayed == 0) + { + Console.WriteLine("No entries found with matching index " + iOriginalIndex + " in the " + this.iLogName + " log"); + } + else if (s_entriesDisplayed == 1) + { + Console.WriteLine("Matching entry found in the " + this.iLogName + " log"); + } + else + { + Console.WriteLine("\nFound " + s_entriesDisplayed + " Matching entries found in the " + this.iLogName + " log"); + } + + } + else + { + if (iPreviousRecordCount > 0) + { + for (EventRecord current = reader.ReadEvent(); (matched < iPreviousRecordCount) && (current != null); current = reader.ReadEvent()) + { + if (iMultiMatch.Length > 0) + { + foreach (string x in iMultiMatch) + { + if (current.ProviderName.Contains(x)) + { + ++matched; + entries.Add(current); + break; // escape at the first match + } + } + } + else + { + if (iSource == "*" || current.ProviderName.Contains(iSource)) + { + ++matched; + entries.Add(current); + } + } + } + + // output matched entries in event log order + OutputEntriesInEventLogOrder(entries); + + } + + if (iTailEventLog) + { + // This is a horrible hack. "dotnet test" does something odd to the console stdin/out/err streams + // in that they always return 'false', 'false', 'true' for Console.IsXXXRedirected no matter what the + // test tries to do. So for now we allow an environment to force console input redirection for testing. + bool testInputRedirected = Environment.GetEnvironmentVariable("EVENTLOGMONITOR_INPUT_REDIRECTED") != null; + bool inputRedirected = false; + if (Console.IsInputRedirected || testInputRedirected) + { + inputRedirected = true; + } + + // if we have displayed no previous entries, we should display a simple message + if (s_entriesDisplayed == 0) + { + string toLog = EventSourceAsString(); + Console.WriteLine("Waiting for events from the " + this.iLogName + " log matching the event source '" + toLog + "'."); + if (Console.IsOutputRedirected || testInputRedirected) + { + Console.WriteLine(""); + } + else + { + Console.WriteLine("Press , 'Q' or to exit or press 'S' for current stats..."); + } + } + + iEventLogQuery.ReverseDirection = false; // cannot tail a log with direction reversed! + EventLogWatcher watcher = new(iEventLogQuery); + watcher.EventRecordWritten += new EventHandler(EventLogEventRead); + watcher.Enabled = true; + + // TODO check if VS still has this old problem: + // if we are redirected, we could be running inside Eclipse or Visual Studio as an external tool. + // Or we could be redirected to a file instead. Either way, we can't wait on ReadLine to exit + // as it may return immediately with end of stream (NULL) as it does (did?) in VS. + while (true) + { + ConsoleKey keyPressed; + if (inputRedirected) + { + // We cannot use Console.ReadKey if input is redirected so use the TextReader directly + TextReader textReader = Console.In; + + int inputValue = textReader.Read(); + if (inputValue == -1) + { + break; // EOS, we are done + } + + // we must be upper case as all ConsoleKeys are + char inputChar = Char.ToUpper((char)inputValue); + + // we only accept a limited number of input chars so it is safer to check specifically for now + keyPressed = (int)inputChar switch + { + (int)ConsoleKey.Enter => ConsoleKey.Enter, + (int)ConsoleKey.Escape => ConsoleKey.Escape, + (int)ConsoleKey.Q => keyPressed = ConsoleKey.Q, + (int)ConsoleKey.S => keyPressed = ConsoleKey.S, + _ => ConsoleKey.I, // I for ignored + }; + } + else + { + try + { + // wait for user to exit - pass true to stop key echoing + ConsoleKeyInfo keyPressedInfo = Console.ReadKey(true); // TODO check this works in eclipse... + keyPressed = keyPressedInfo.Key; + } + catch (InvalidOperationException) + { + // this can happen if stdin is redirected to read from a file + inputRedirected = true; + continue; + } + } + + // Console.WriteLine("KeyPressed: '" + keyPressed + "'"); // debug + if (keyPressed == ConsoleKey.Enter || keyPressed == ConsoleKey.Escape || keyPressed == ConsoleKey.Q) + { + Console.WriteLine(); + break; // we are done + } + + // allow a single 's' to mean 'show stats and continue' + if (keyPressed == ConsoleKey.S) + { + // TODO more stats (count of warning, errors etc) + Console.ResetColor(); + Console.WriteLine(s_entriesDisplayed + " Entries shown so far from the " + this.iLogName + " log. Waiting for more events..."); + } + + // ignore other key presses + } + + } + + // Always finish by showing what we displayed + string toLog2 = EventSourceAsString(); + Console.WriteLine(s_entriesDisplayed + " Entries shown from the " + this.iLogName + " log matching the event source '" + toLog2 + "'."); + } + } + + // mostly displays any entry passed in... + private bool DisplayEventLogEntry(EventRecord entry) + { + bool brokerEventLogEntry = IsBrokerEntry(entry.ProviderName); + + StandardEventLevel level = StandardEventLevel.LogAlways; + if (entry.Level.HasValue) + { + level = (StandardEventLevel)entry.Level; + } + String type; + ConsoleColor textColour; + if (entry.LogName == "Security") + { + if( ((ulong)entry.Keywords ^ 0x8010000000000000) == 0x0) { + type = "F"; textColour = ConsoleColor.Red; // Audit Failure + } else { + type = "S"; textColour = ConsoleColor.Green; // Audit Success + } + } + else + { + switch (level) + { + case StandardEventLevel.Informational: type = "I"; textColour = ConsoleColor.Green; break; + case StandardEventLevel.Warning: type = "W"; textColour = ConsoleColor.Yellow; break; + case StandardEventLevel.Error: type = "E"; textColour = ConsoleColor.Red; break; + case StandardEventLevel.Critical: type = "C"; textColour = ConsoleColor.DarkRed; break; + case StandardEventLevel.LogAlways: type = "I"; textColour = ConsoleColor.Green; break; + case StandardEventLevel.Verbose: type = "V"; textColour = ConsoleColor.Green; break; + default: type = "I"; textColour = ConsoleColor.Green; break; + } + } + + String message = string.Empty; + String win32Message = String.Empty; + try + { + + if (iCultureSet) + { + // try to get the specific language version of the message. + // however, this may not be available so the message may not be found + CultureSpecificMessage.EventLogRecordWrapper wrapper = new(entry); + message = CultureSpecificMessage.GetCultureSpecificMessage(wrapper, iChosenCulture.LCID); + + if (string.IsNullOrEmpty(message)) + { + // try again with the console default culture instead + message = entry.FormatDescription(); + } + + } + else + { + message = entry.FormatDescription(); + + // retry with US culture as this tries different places to find a catalogue + if (string.IsNullOrEmpty(message)) + { + // try to get the specific language version of the message. + // however, this may not be available so the message may not be found + CultureSpecificMessage.EventLogRecordWrapper wrapper = new(entry); + message = CultureSpecificMessage.GetCultureSpecificMessage(wrapper, iUSDefaultCulture.LCID); + } + } + + } + catch (EventLogException) + { + // Console.WriteLine(e.ToString()); + } + + // see if we still have nothing! + if (string.IsNullOrEmpty(message)) + { + // build our own response message like the event log API does! + message = "The description for Event ID " + entry.Id + " from source " + entry.ProviderName + " cannot be found. " + + "Either the component that raises this event is not installed on your local computer or the installation is corrupted. " + + "You can install or repair the component on the local computer.\r\n\r\n" + + "If the event originated on another computer, the display information had to be saved with the event.\r\n\r\n"; + + bool first = true; + foreach (EventProperty prop in entry.Properties) + { + if (first) + { + message += "The following information was included with the event:\r\n"; + first = false; + } + + // byte[] will normally be binary data, not an insert! + if (prop.Value.GetType() != typeof(byte[])) + { + message += prop.Value.ToString(); + } + message += "\r\n"; + } + + message += "The message resource is present but the message was not found in the message table"; + + if (entry.Qualifiers == 0) + { + // So this is a quick and simple way to get the text for a win32 error code. + // We could call our own FormatMessage but that would be more effort and I'm + // not sure there would be any extra benefit. + Win32Exception win32Error = new(entry.Id); + win32Message = win32Error.Message; + } + } + + // full is everything (description, explanation and user action) and we output it "as is" + if (!iFullOutput) + { + // for medium and minimal we trim the beginning to make sure we are starting at real text + // as some events start with a leading space or a \r\n\r\n which throws off the splitting. + message = message.TrimStart(); + if (iMediumOutput) + { + // description and explanation + int index = message.IndexOf(iEventLongSeparater); + if (index > 0) + { + index = message.IndexOf(iEventLongSeparater, index + iEventLongSeparater.Length); + if (index > 0) + { + message = message[0..index]; + } + } + else + { + // some events only use \r\n + index = message.IndexOf(iEventShortSeparater); + if (index > 0) + { + index = message.IndexOf(iEventShortSeparater, index + iEventShortSeparater.Length); + if (index > 0) + { + message = message[0..index]; + } + } + } + } + else + { + //minimal is just description (this is the default) + int index = message.IndexOf(iEventLongSeparater); + if (index > 0) + { + message = message[0..index]; + } + else + { + // some events only use \r\n + index = message.IndexOf(iEventShortSeparater); + if (index > 0) + { + message = message[0..index]; + } + } + + // for minimal we only want one line if possible, so remove any line breaks + message = message.Replace("\r\n", ""); + } + } + + // remove any trailing junk + message = message.TrimEnd(iTrimChars); + + // check to see if we need to filter out the entry, now we have formatted it. + if (!string.IsNullOrEmpty(iEntryInclusiveFilter)) + { + if (!message.Contains(iEntryInclusiveFilter)) + { + // Console.Write("Ignoring inc entry: " + entry.Id + "\n"); + return false; + } + } + + if (!string.IsNullOrEmpty(iEntryExclusiveFilter)) + { + if (message.Contains(iEntryExclusiveFilter)) + { + // Console.Write("Ignoring exc entry: " + entry.Id + "\n"); + return false; + } + } + + if (iTimestampFirst) + { + Console.ForegroundColor = ConsoleColor.White; + Console.Write(entry.TimeCreated + "." + entry.TimeCreated.Value.Millisecond + ": "); + Console.ResetColor(); + } + + Console.ForegroundColor = textColour; + if (brokerEventLogEntry) + { + Console.Write("BIP" + entry.Id + type + ": "); + } + else + { + Console.Write(entry.Id + type + ": "); + } + + bool forceMinBinaryOutput = false; + if (level == StandardEventLevel.Error || level == StandardEventLevel.Critical) + { + // force binary tracing for errors + forceMinBinaryOutput = true; + } + + Console.ResetColor(); + Console.Write(message); + if (!iTimestampFirst) + { + Console.ForegroundColor = ConsoleColor.White; + Console.Write(" [" + entry.TimeCreated + "." + entry.TimeCreated.Value.Millisecond + "]\n"); + Console.ResetColor(); + } + else + { + Console.Write("\n"); + } + + if (iVerboseOutput) + { + EventLogRecord logRecord = (EventLogRecord)entry; + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.Write("Machine: {0}. Log: {1}. Source: {2}.", logRecord.MachineName, logRecord.ContainerLog, logRecord.ProviderName); + + if (logRecord.UserId != null) + { + string name = logRecord.UserId.ToString(); + Console.Write(" User: {0}.", name); + } + + if (logRecord.ProcessId != 0) + { + string procId = logRecord.ProcessId.ToString(); + Console.Write(" ProcessId: {0}.", procId); + } + + if (logRecord.ThreadId != 0) + { + string tId = logRecord.ThreadId.ToString(); + Console.Write(" ThreadId: {0}.", tId); + } + + if (logRecord.Version != 0) + { + string ver = logRecord.Version.ToString(); + Console.Write(" Version: {0}.", ver); + } + + if (!string.IsNullOrEmpty(win32Message)) + { + Console.Write(" Win32Msg: {0} ({1}).", win32Message, entry.Id); + } + + Console.WriteLine(); // finish with a blank line + Console.ResetColor(); + } + + if (iMinBinaryOutput || iFullBinaryOutput || forceMinBinaryOutput) + { + byte[] data = null; + int count = entry.Properties.Count; + if (count > 0) + { + data = entry.Properties[count - 1].Value as byte[]; + } + if (iFullBinaryOutput) + { + BinaryDataFormatter.OutputFormattedBinaryData(data, (long)entry.RecordId); + } + else + { + BinaryDataFormatter.OutputFormattedBinaryDataAsString(data, (long)entry.RecordId); + } + } + + return true; + } + + static void Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + + // create and execute the monitoring object + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + if (initialized) + { + monitor.MonitorEventLog(); + } + } + + public bool Initialize(string[] args) + { + SimpleArgumentProcessor myArgs = new(args); + bool ok = ParseArguments(myArgs); + if (!ok) + { + return false; + } + + // test platform (XP needs an older API) + OperatingSystem os = Environment.OSVersion; + Version version = os.Version; + if ((os.Platform == PlatformID.Win32NT) && (version.Major < 6)) + { + Console.WriteLine("EventLogMonitor is not supported on this platform; use Windows 7 or above."); + return false; + } + + iInitialized = true; + s_entriesDisplayed = 0; + return true; + } + + private static bool IsBrokerEntry(string toMatch) + { + foreach (string x in iBrokerMatch) + { + if (toMatch.Contains(x)) + { + return true; // escape at the first match + } + } + return false; + } + + public void MonitorEventLog() + { + if (!iInitialized) + { + // it would be an error by the caller to get there + DisplayHelp(); + return; + } + + try + { + if (iDisplayLogs) + { + DisplayAvailableLogs(); + } + else + { + DisplayMatchingEvents(); + } + } + catch (EventLogNotFoundException e) + { + Console.WriteLine(e.Message); + if (e.Message.Contains("could not be found")) + { + Console.WriteLine("Make sure the event log '" + iLogName + "' exists."); + } + } + catch (EventLogException e) + { + // general error + Console.WriteLine(e.Message); + if (iVerboseOutput) + { + Console.WriteLine(e.StackTrace); + } + } + catch (InvalidOperationException e) + { + Console.WriteLine(e.Message); + if (e.Message.Contains("does not exist")) + { + Console.WriteLine("Make sure the event log '" + iLogName + "' exists."); + } + } + catch (SecurityException e) + { + Console.WriteLine(e.Message); + if (iLogName.ToLower().Equals("security")) + { + // we know its Security + Console.WriteLine("Run from an elevated command prompt to access the 'Security' event log."); + } + else + { + // probably a similar problem + Console.WriteLine("Try running from an elevated command prompt to access the '" + iLogName + "' log."); + } + } + catch (UnauthorizedAccessException e) + { + Console.WriteLine(e.Message); + if (iLogName.ToLower().Equals("security")) + { + // we know its Security + Console.WriteLine("Run from an elevated command prompt to access the 'Security' event log."); + } + else + { + // probably a similar problem + Console.WriteLine("Try running from an elevated command prompt to access the '" + iLogName + "' log."); + } + } + catch (Exception e) + { + // unknown general error + Console.WriteLine("\n" + e.Message); + if (iVerboseOutput) + { + Console.WriteLine(e.StackTrace); + } + Console.WriteLine("Problem encountered.\nPlease raise an issue at https://github.com/m-g-k/EventLogMonitor/issues for a fix"); + } + finally + { + // we may well have an exception that means we leave the colour set, so make sure we always reset it. + Console.ResetColor(); + } + } + + readonly private Char[] iTrimChars = new Char[] { ' ', '\n', '\t', '\r' }; + readonly private string iEventLongSeparater = "\r\n\r\n"; + readonly private string iEventShortSeparater = "\r\n"; + private bool iMinimalOutput = true; //the default + private bool iMediumOutput = false; + private bool iFullOutput = false; + private bool iMinBinaryOutput = false; + private bool iFullBinaryOutput = false; + private bool iTailEventLog = true; + private bool iVerboseOutput = false; + private string iSource = ""; // default to WMB + private static readonly string[] iBrokerMatch = { "IBM Integration", "WebSphere Broker", "IBM App Connect Enterprise" }; + private string[] iMultiMatch = iBrokerMatch; // initially we assume broker + private string iLogName = "Application"; + private string iDefaultCulture = "en-US"; + private string iEntryInclusiveFilter = ""; + private string iEntryExclusiveFilter = ""; + private bool iCultureSet = false; + private bool iTimestampFirst = false; + private uint iOriginalIndex = 0; + private uint iRecordIndexMin = 0; + private uint iRecordIndexMax = 0; + private uint iRecordIndexRange = 0; + private uint iPreviousRecordCount = 0; + private CultureInfo iChosenCulture = null; + private CultureInfo iUSDefaultCulture = null; + private EventLogQuery iEventLogQuery = null; + private bool iDisplayLogs; + private int iLogLevel = -1; // include every type of event + private bool iInitialized = false; + + private static void DisplayHelp() + { + Console.WriteLine("EventLogMonitor : Version {0} : https://github.com/m-g-k/EventLogMonitor", GetProductVersion()); + Console.WriteLine("Usage 1 : EventLogMonitor [-p ] [-1|-2|-3] [-s ] [-nt] [-v]"); + Console.WriteLine(" [-b1] [-b2] [-l ] [-c ] [-tf]"); + Console.WriteLine(" [-fi ] [-fx ] [-fw | -fe]"); + Console.WriteLine("Usage 2 : EventLogMonitor -i index [-v] [p ] [-c ]"); + Console.WriteLine(" [-b1] [-b2] [-fi ] [-fx ] [-fw | -fe]"); + Console.WriteLine(" [-1|-2|-3] [-l ] [-tf]"); + Console.WriteLine("Usage 3 : EventLogMonitor -d [-v] [-l ]"); + Console.WriteLine(" e.g. EventLogMonitor -p 10 -2"); + Console.WriteLine(" e.g. EventLogMonitor -i 115324 -b1"); + Console.WriteLine(" e.g. EventLogMonitor -p 5 -s * -l System"); + Console.WriteLine(" e.g. EventLogMonitor -p 10 -s \"Integration,MQ\" -b1 -2"); + Console.WriteLine(" e.g. EventLogMonitor -p * -s \"Browser Agent\" -l c:\\temp\\mylog.evtx"); + Console.WriteLine(" e.g. EventLogMonitor -i \"1127347-1127350\" -b1"); + Console.WriteLine(" -p show the last entries from the event log for the given ."); + Console.WriteLine(" Use a '*' for all event log entries."); + Console.WriteLine(" -1 Minimal output (description only). This is the default output format."); + Console.WriteLine(" -2 Medium output (description and explanation only)."); + Console.WriteLine(" -3 Full output (description, explanation and user action)."); + Console.WriteLine(" -b1 Show any binary info as text followed by the index of the entry."); + Console.WriteLine(" -b2 Show any binary info as a hex dump followed by the index of the entry."); + Console.WriteLine(" -nt Not tailing - tailing the event log is the default for usage 1."); + Console.WriteLine(" -tf Display the timestamp first rather than last for each entry shown."); + Console.WriteLine(" -i Display an entry with a specific index. Use -b1 to display indexes."); + Console.WriteLine(" Display a range of events by specifying the start and end of the range."); + Console.WriteLine(" For example, -i \"1127347-1127350\". Alternatively specify the index and a"); + Console.WriteLine(" count with -p, E.G. -p 5 -i 127347 displays the 5 events before and after"); + Console.WriteLine(" the index, with the index event in the middle assuming the other events"); + Console.WriteLine(" exist."); + Console.WriteLine(" -l Event log name to view or tail. Defaults to the Application log."); + Console.WriteLine(" Use a relative or an absolute path to use a log file (*.evtx) instead."); + Console.WriteLine(" -v Verbose output. Outputs extra details (if present) for each record."); + Console.WriteLine(" -c Culture, defaults to \"En-US\". Options include \"De-DE\",\"Es-ES\",\"Fr-FR\""); + Console.WriteLine(" \"It-IT\",\"Ja-JP\",\"Ko-KR\",\"Pl-PL\",\"Pt-BR\",\"Ru-RU\",\"Tr-TR\",\"Zh-CN\",\"Zh-TW\"."); + Console.WriteLine(" -s Specify the log source name. Defaults to entries from ACE, IIB and WMB."); + Console.WriteLine(" A partial name can also be used, so 'IBM' matches any entry containing"); + Console.WriteLine(" IBM. Use a '*' for all event log sources. Use a ',' to specify multiple"); + Console.WriteLine(" sources."); + Console.WriteLine(" For example, \"Integration, MQ\" will match all IIB, and WMQ entries."); + Console.WriteLine(" -d Displays details of available event logs. Add -v for extra information."); + Console.WriteLine(" Specify -d [-v] -l for details on a log file."); + Console.WriteLine(" Specify -d [-v] -l \"app, Hyper\" to filter on a subset of logs."); + Console.WriteLine(" -fi Specify -fi to only show entries that contain ."); + Console.WriteLine(" -fx Specify -fx to only show entries that do not contain ."); + Console.WriteLine(" -fw Specify -fw to only show entries that are 'Warnings' or 'Errors'."); + Console.WriteLine(" -fe Specify -fe to only show entries that are 'Errors'."); + Console.WriteLine(" -version - displays the version of this tool."); + Console.WriteLine(" -? or -help - displays this help."); + Console.WriteLine("When tailing the event log at the console, press , 'Q' or to exit."); + } + + private static void DisplayVersion() + { + Console.WriteLine("EventLogMonitor version {0}", GetProductVersion()); + } + + private static string GetProductVersion() + { + int majorVersion = typeof(EventLogMonitor).Assembly.GetName().Version.Major; + int minorVersion = typeof(EventLogMonitor).Assembly.GetName().Version.Minor; + return string.Format("{0}.{1}", majorVersion, minorVersion); + } + public void EventLogEventRead(object sender, EventArgs x1) + { + // Make sure there was no error reading the event and get the record from it + EventRecord entry; + if (x1 is EventRecordWrittenEventArgs x2) + { + if (x2.EventRecord is null) + { + Console.WriteLine("The event instance was null 1."); + return; + } + if (x2.EventException != null) + { + Console.WriteLine("Exception getting event 1" + x2.EventException.ToString()); + return; + } + entry = x2.EventRecord; + } + else if (x1 is MyEventRecordWrittenEventArgs x3) + { + if (x3.EventRecord is null) + { + Console.WriteLine("The event instance was null 2."); + return; + } + if (x3.EventException != null) + { + Console.WriteLine("Exception getting event 2" + x3.EventException.ToString()); + return; + } + entry = x3.EventRecord; + } + else + { + Console.WriteLine("Unknown EventType"); + return; + } + + bool match = false; + if (iMultiMatch.Length > 0 && iRecordIndexMin == 0) + { + foreach (string x in iMultiMatch) + { + if (entry.ProviderName.Contains(x)) + { + match = true; + break; // escape at the first match + } + } + } + else if (iSource == "*" || entry.ProviderName.Contains(iSource) || iRecordIndexMin > 0) + { + match = true; + } + + if (match) + { + // display it! + bool displayed = DisplayEventLogEntry(entry); + if (displayed) + { + ++s_entriesDisplayed; // increment counts + } + } + + } + + private void OutputEntriesInEventLogOrder(List entries) + { + // output matched entries in event log order + if (entries.Count > 0) + { + entries.Reverse(); // reorder correctly + foreach (EventRecord entry in entries) + { + bool displayed = DisplayEventLogEntry(entry); + if (displayed) + { + ++s_entriesDisplayed; // increment counts + } + } + } + } + + public string EventSourceAsString() + { + string toLog = ""; + bool first = true; + if (iMultiMatch.Length > 0) + { + foreach (string x in iMultiMatch) + { + if (first) + { + toLog = x; + first = false; + } + else + { + toLog = toLog + "' or '" + x; + } + } + } + else + { + toLog = iSource; + } + return toLog; + } + + public static bool LogIsAFile(string logName) + { + // if it looks like a file... + return !string.IsNullOrEmpty(logName) && (logName.Contains(':') || logName.Contains('\\') || logName.Contains('.')); + } + + private static bool LogNameMatch(string logName, string[] logsToMatch, bool matchAll) + { + if (matchAll) + { + return true; + } + + foreach (string current in logsToMatch) + { + if (logName.Contains(current, StringComparison.CurrentCultureIgnoreCase)) + { + return true; + } + } + return false; + } + + // class used to aid testing + public abstract class MyEventRecordWrittenEventArgs : EventArgs + { + public EventRecord EventRecord; + + public Exception EventException; + } + + +} + diff --git a/src/EventLogMonitor/SimpleCommandParser.cs b/src/EventLogMonitor/SimpleCommandParser.cs new file mode 100644 index 0000000..2acc7ba --- /dev/null +++ b/src/EventLogMonitor/SimpleCommandParser.cs @@ -0,0 +1,324 @@ +/* + Copyright 2012-2022, MGK + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Generic; + +namespace EventLogMonitor; +public class SimpleArgumentProcessor +{ + public SimpleArgumentProcessor(string[] args) + { + this.iTotalArguments = args.Length; + this.iRequiredUnFlaggedArgumentsCount = 0; + this.iOptionalUnFlaggedArgumentsCount = 0; + this.iTotalFlaggedArguments = 0; + this.iTotalBooleanArguments = 0; + this.iTotalInvalidArgumentCount = 0; + + string currentFlag = ""; + for (int i = 0; i < iTotalArguments; ++i) + { + string currentArgument = args[i]; + if (currentArgument.Length == 0) + { + continue; // skip an empty argument + } + + iAllArguments.Add(currentArgument); + if (currentArgument[0].Equals('-') || currentArgument[0].Equals('/')) + { + // currentArgument = "-" + currentArgument.Substring(1); //force to be a '-' + currentArgument = "-" + currentArgument[1..]; // force to be a '-' + + // flagged + if (!(currentFlag.Length == 0)) + { + // we have an empty flagged argument (boolean) + + // check it does not already exist + if (iFlaggedArguments.ContainsKey(currentFlag)) + { + ++iTotalInvalidArgumentCount; // duplicate argument(s) found. Error is noticed in validate call + } + else + { + iFlaggedArguments.Add(currentFlag, ""); + } + + ++iTotalBooleanArguments; + } + currentFlag = currentArgument; + } + else + { + // unflagged + if (currentFlag.Length == 0) + { + iUnflaggedArguments.Add(currentArgument); + } + else + { + // flagged (with value) + + // check it does not already exist + if (iFlaggedArguments.ContainsKey(currentFlag)) + { + ++iTotalInvalidArgumentCount; // duplicate argument(s) found. Error is noticed in validate call + } + else + { + iFlaggedArguments.Add(currentFlag, currentArgument); + } + + currentFlag = ""; + ++iTotalFlaggedArguments; + } + } + } + + // special case a dangling flagged argument + if (!(currentFlag.Length == 0)) + { + // we have an empty flagged argument left over + + // check it does not already exist + if (iFlaggedArguments.ContainsKey(currentFlag)) + { + ++iTotalInvalidArgumentCount; // duplicate argument(s) found. Error is noticed in validate call + } + else + { + iFlaggedArguments.Add(currentFlag, ""); + } + + ++iTotalBooleanArguments; + } + } + + public List GetAllArgs() + { + return iAllArguments; + } + + public string GetFlaggedArgument(string flag) + { + string match = ""; + if (iFlaggedArguments.ContainsKey(flag)) + { + match = iFlaggedArguments[flag]; + } + + return match; + } + + public string GetUnFlaggedArgument(int index) + { + string match = ""; + if (index < iUnflaggedArguments.Count) + { + match = iUnflaggedArguments[index]; + } + + return match; + } + + public bool GetBooleanArgument(string flag) + { + bool match = false; + + if (iFlaggedArguments.ContainsKey(flag)) + { + match = true; + } + + return match; + } + + // Note: a required argument needs a value, a boolean one does not (to be valid) + public void SetRequiredFlaggedArgument(string args) { iRequiredFlaggedArguments.Add(args); } + public void SetOptionalFlaggedArgument(string args) { iOptionalFlaggedArguments.Add(args); } + public void SetRequiredBooleanArgument(string args) { iRequiredBooleanArguments.Add(args); } + public void SetOptionalBooleanArgument(string args) { iOptionalBooleanArguments.Add(args); } + public void SetRequiredUnFlaggedArgumentCount(int count) { iRequiredUnFlaggedArgumentsCount = count; } + public void SetOptionalUnFlaggedArgumentCount(int count) { iOptionalUnFlaggedArgumentsCount = count; } + + public bool ValidateArguments(bool debug = false) + { + // validate the unflagged args next + int totalUnflaggedArgs = iUnflaggedArguments.Count; + int totalValidArguments = iRequiredUnFlaggedArgumentsCount + iOptionalUnFlaggedArgumentsCount; + if (totalUnflaggedArgs < iRequiredUnFlaggedArgumentsCount || + totalUnflaggedArgs > totalValidArguments) + { + if (debug) { Console.WriteLine("Invalid unflagged argument count. Max expected: {0} , Received: {1}\n", totalValidArguments, totalUnflaggedArgs); } + return false; + } + + // validate required flagged arguments. We work on a copy so we can erase as we find + Dictionary flaggedArgumentsCopy = new(iFlaggedArguments); + { + foreach (string current in iRequiredFlaggedArguments) + { + if (!flaggedArgumentsCopy.ContainsKey(current)) + { + // not found + if (debug) { Console.WriteLine("Required argument '{0}' not found\n", current); } + return false; + } + + string value = flaggedArgumentsCopy[current]; + if (string.IsNullOrEmpty(value)) + { + // expected value for flagged argument + if (debug) { Console.WriteLine("Required argument '{0}' has no value\n", current); } + return false; + } + + flaggedArgumentsCopy.Remove(current); + } + } + + // validate required boolean arguments + { + foreach (string current in iRequiredBooleanArguments) + { + if (!flaggedArgumentsCopy.ContainsKey(current)) + { + // not found + if (debug) { Console.WriteLine("Required boolean argument '{0}' not found\n", current); } + return false; + } + + string value = flaggedArgumentsCopy[current]; + if (!string.IsNullOrEmpty(value)) + { + // unexpected value for boolean argument + if (debug) { Console.WriteLine("Required boolean argument '{0}' has an unexpected value '{1}'\n", current, value); } + return false; + } + + flaggedArgumentsCopy.Remove(current); + } + } + + // validate optional flagged arguments. + { + foreach (string current in iOptionalFlaggedArguments) + { + // erase if found and not empty + if (!flaggedArgumentsCopy.ContainsKey(current)) + { + // not found + continue; + } + + string value = flaggedArgumentsCopy[current]; + if (string.IsNullOrEmpty(value)) + { + // expected value for flagged argument + if (debug) { Console.WriteLine("Optional argument '{0}' has no value\n", current); } + return false; + } + + flaggedArgumentsCopy.Remove(current); + } + } + + // validate optional boolean arguments. + { + foreach (string current in iOptionalBooleanArguments) + { + // erase if found and not empty + if (!flaggedArgumentsCopy.ContainsKey(current)) + { + // not found + continue; + } + + string value = flaggedArgumentsCopy[current]; + if (!string.IsNullOrEmpty(value)) + { + // unexpected value for boolean argument + if (debug) { Console.WriteLine("Optional boolean argument '{0}' has an unexpected value '{1}'\n", current, value); } + return false; + } + + flaggedArgumentsCopy.Remove(current); + } + } + + // see if we have any other arguments left over... + if (flaggedArgumentsCopy.Count > 0) + { + // unexpected arguments present + if (debug) + { + Console.WriteLine("Unexpected arguments found: "); + bool first = true; + foreach (KeyValuePair current in flaggedArgumentsCopy) + { + if (!first) + { + Console.WriteLine(", "); + } + else + { + first = false; + } + if (string.IsNullOrEmpty(current.Value)) + { + Console.WriteLine("'{0}'", current.Key); + } + else + { + Console.WriteLine("'{0}' : '{1}'", current.Key, current.Value); + } + } + Console.WriteLine("\n"); + } + return false; + } + + // finally see if we have any errors already (do this last as the duplicate flags could be invalid ones...) + if (iTotalInvalidArgumentCount != 0) + { + // error - invalid arguments found (multiple flags with same name entered) + if (debug) { Console.WriteLine("Invalid arguments found, {0} flag(s) entered more than once.\n", iTotalInvalidArgumentCount); } + return false; + } + + // if we get here, we made it! + if (debug) { Console.WriteLine("All provided arguments validated ok!\n"); } + return true; + } + + readonly private int iTotalArguments; + readonly private int iTotalFlaggedArguments; + readonly private int iTotalBooleanArguments; + readonly private int iTotalInvalidArgumentCount; + readonly private List iAllArguments = new(); + readonly private List iUnflaggedArguments = new(); + readonly private Dictionary iFlaggedArguments = new(); + + readonly private List iRequiredFlaggedArguments = new(); + readonly private List iOptionalFlaggedArguments = new(); + readonly private List iRequiredBooleanArguments = new(); + readonly private List iOptionalBooleanArguments = new(); + private int iRequiredUnFlaggedArgumentsCount; + private int iOptionalUnFlaggedArgumentsCount; + +} \ No newline at end of file diff --git a/test/EventLogMonitorTests/BinaryDataFormatterTests.cs b/test/EventLogMonitorTests/BinaryDataFormatterTests.cs new file mode 100644 index 0000000..6f2ccf0 --- /dev/null +++ b/test/EventLogMonitorTests/BinaryDataFormatterTests.cs @@ -0,0 +1,479 @@ +/* + Copyright 2012-2022, MGK + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using Xunit; +using Xunit.Abstractions; +using System; +using System.IO; +using System.Diagnostics.CodeAnalysis; + +namespace EventLogMonitor; + +[Collection("EventLogMonitor")] +public class BinaryDataFormatterTests +{ + [SuppressMessage("Microsoft.Usage", "IDE0052:RemoveUnreadPrivateMember", MessageId = "stdoutput")] + private readonly ITestOutputHelper stdoutput; + public BinaryDataFormatterTests(ITestOutputHelper testOutputHelper) + { + stdoutput = testOutputHelper; + } + + [Fact] + public void ZeroBytesPasedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = Array.Empty(); + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 0); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.False(ret); + Assert.Single(lines); + Assert.Equal(". Index: 0", lines[0]); + } + + [Fact] + public void NullPasedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(null, 0); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.False(ret); + Assert.Single(lines); + Assert.Equal(". Index: 0", lines[0]); + } + + [Fact] + public void ZeroBytesPasedInIsAllowedAsString() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = Array.Empty(); + bool ret = BinaryDataFormatter.OutputFormattedBinaryDataAsString(input, 0); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.False(ret); + Assert.Single(lines); + Assert.Equal(". Index: 0", lines[0]); + } + + [Fact] + public void NullPasedInIsAllowedAsString() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + bool ret = BinaryDataFormatter.OutputFormattedBinaryDataAsString(null, 0); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.False(ret); + Assert.Single(lines); + Assert.Equal(". Index: 0", lines[0]); + } + + [Fact] + public void ControlCharIsSeenAsBinaryForString() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x10 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryDataAsString(input, 1); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Single(lines); + Assert.Equal(". Index: 1", lines[0]); + } + + [Fact] + public void InvalidUnicodeIsHandledForString() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0xCD, 0xDF }; // invalid unicode sequence which forces an exception in the convertor + bool ret = BinaryDataFormatter.OutputFormattedBinaryDataAsString(input, 1); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Single(lines); + Assert.Equal(". Index: 1", lines[0]); + } + + [Fact] + public void OneBytePassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x41 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 1); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 1", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000001: 41 A", lines[2]); + Assert.Equal("Index: 1", lines[3]); + } + + [Fact] + public void TwoBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x41, 0x42 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 2); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 2", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000002: 41 42 AB", lines[2]); + Assert.Equal("Index: 2", lines[3]); + } + + [Fact] + public void ThreeBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x41, 0x42, 0x43 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 3); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 3", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000003: 41 42 43 ABC", lines[2]); + Assert.Equal("Index: 3", lines[3]); + } + + [Fact] + public void FourBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x41, 0x42, 0x43, 0x44 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 4); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 4", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000004: 41 42 43 44 ABCD 44434241", lines[2]); + Assert.Equal("Index: 4", lines[3]); + } + + [Fact] + public void FiveBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x41, 0x42, 0x43, 0x44, 0x45 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 5); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 5", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000005: 41 42 43 44-45 ABCDE 44434241", lines[2]); + Assert.Equal("Index: 5", lines[3]); + } + + [Fact] + public void SixBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 6); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 6", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000006: 41 42 43 44-45 46 ABCDEF 44434241", lines[2]); + Assert.Equal("Index: 6", lines[3]); + } + + [Fact] + public void SevenBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 7); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 7", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000007: 41 42 43 44-45 46 47 ABCDEFG 44434241", lines[2]); + Assert.Equal("Index: 7", lines[3]); + } + + [Fact] + public void EightBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 8); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 8", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000008: 41 42 43 44-45 46 47 48 ABCDEFGH 44434241 48474645", lines[2]); + Assert.Equal("Index: 8", lines[3]); + } + + // test below are with unprintable characters + + [Fact] + public void OneUnprintableBytePassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x00 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 1); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 1", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000001: 00 .", lines[2]); + Assert.Equal("Index: 1", lines[3]); + } + + [Fact] + public void TwoUnprintableBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x01, 0x02 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 2); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 2", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000002: 01 02 ..", lines[2]); + Assert.Equal("Index: 2", lines[3]); + } + + [Fact] + public void ThreeUnprintableBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x03, 0x04, 0x05 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 3); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 3", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000003: 03 04 05 ...", lines[2]); + Assert.Equal("Index: 3", lines[3]); + } + + [Fact] + public void FourUnprintableBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x06, 0x07, 0x08, 0x09 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 4); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 4", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000004: 06 07 08 09 .... 09080706", lines[2]); + Assert.Equal("Index: 4", lines[3]); + } + + [Fact] + public void FiveUnprintableBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x10, 0x11, 0x12, 0x13, 0x14 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 5); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 5", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000005: 10 11 12 13-14 ..... 13121110", lines[2]); + Assert.Equal("Index: 5", lines[3]); + } + + [Fact] + public void SixUnprintableBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x15, 0x16, 0x17, 0x18, 0x19, 0x80 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 6); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 6", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000006: 15 16 17 18-19 80 ...... 18171615", lines[2]); + Assert.Equal("Index: 6", lines[3]); + } + + [Fact] + public void SevenUnprintableBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 7); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 7", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000007: 81 82 83 84-85 86 87 ....... 84838281", lines[2]); + Assert.Equal("Index: 7", lines[3]); + } + + [Fact] + public void EightUnprintableBytesPassedInIsAllowed() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + byte[] input = new byte[] { 0x88, 0x89, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95 }; + bool ret = BinaryDataFormatter.OutputFormattedBinaryData(input, 8); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.True(ret); + Assert.Equal(4, lines.Length); + Assert.Equal("Binary Data size: 8", lines[0]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[1]); + Assert.Equal("00000008: 88 89 90 91-92 93 94 95 ........ 91908988 95949392", lines[2]); + Assert.Equal("Index: 8", lines[3]); + } + +} \ No newline at end of file diff --git a/test/EventLogMonitorTests/CultureSpecificMessageTests.cs b/test/EventLogMonitorTests/CultureSpecificMessageTests.cs new file mode 100644 index 0000000..b579d1b --- /dev/null +++ b/test/EventLogMonitorTests/CultureSpecificMessageTests.cs @@ -0,0 +1,81 @@ +/* + Copyright 2012-2022, MGK + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using Xunit; +using Xunit.Abstractions; +using Moq; +using System.Diagnostics.Eventing.Reader; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System; +using System.IO; + +namespace EventLogMonitor; + +[Collection("EventLogMonitor")] +public class CultureSpecificMessageTests +{ + [SuppressMessage("Microsoft.Usage", "IDE0052:RemoveUnreadPrivateMember", MessageId = "stdoutput")] + private readonly ITestOutputHelper stdoutput; + private readonly string vSSSampleEventLog; + private readonly string invalidMessageCatalogueDll; + + public CultureSpecificMessageTests(ITestOutputHelper testOutputHelper) + { + stdoutput = testOutputHelper; + vSSSampleEventLog = "../../../../../test/EventLogMonitorTests/SampleEventLogs/VSS-Log.evtx"; + invalidMessageCatalogueDll = "../../../../../test/EventLogMonitorTests/SampleEventLogs/InvalidMessageCatalogue.dll"; + } + + [Fact] + public void MessageFromMessageCatalogueDllReturned() + { + // test we can pull out a raw message from a message catalogue dll + var mockEventRecord = new Mock(); + mockEventRecord.Setup(x => x.Id).Returns(8224); // this must be a valid info message number + mockEventRecord.Setup(x => x.LogName).Returns("Applicationz"); + mockEventRecord.Setup(x => x.ProviderName).Returns("I Dont Exist"); + mockEventRecord.Setup(x => x.ContainerLog).Returns(vSSSampleEventLog); // valid event log name name + mockEventRecord.Setup(x => x.Qualifiers).Returns(0); + mockEventRecord.Setup(x => x.Properties).Returns(new List()); + int enUSCulture = 1033; + + string message = CultureSpecificMessage.GetCultureSpecificMessage(mockEventRecord.Object, enUSCulture); + stdoutput.WriteLine(message); + + Assert.Equal("The VSS service is shutting down due to idle timeout. %1 ", message); + } + + [Fact] + public void InvalidMessageCatalogueReturnsAnEmptyString() + { + // test an invalid message catalogue is ignored + var mockEventRecord = new Mock(); + mockEventRecord.Setup(x => x.Id).Returns(42); + mockEventRecord.Setup(x => x.LogName).Returns("Applicationz"); + mockEventRecord.Setup(x => x.ProviderName).Returns("I Dont Exist"); + mockEventRecord.Setup(x => x.ContainerLog).Returns(invalidMessageCatalogueDll); + mockEventRecord.Setup(x => x.Qualifiers).Returns(0); + mockEventRecord.Setup(x => x.Properties).Returns(new List()); + int enUSCulture = 1033; + + string message = CultureSpecificMessage.GetCultureSpecificMessage(mockEventRecord.Object, enUSCulture); + stdoutput.WriteLine(message); + + Assert.Empty(message); + } + +} \ No newline at end of file diff --git a/test/EventLogMonitorTests/EventLogMonitorTests.cs b/test/EventLogMonitorTests/EventLogMonitorTests.cs new file mode 100644 index 0000000..d2ba1bc --- /dev/null +++ b/test/EventLogMonitorTests/EventLogMonitorTests.cs @@ -0,0 +1,1377 @@ +/* + Copyright 2012-2022, MGK + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using Xunit; +using Xunit.Abstractions; +using System; +using System.IO; +using System.Diagnostics.CodeAnalysis; +using Moq; +using System.Diagnostics.Eventing.Reader; +using System.Collections.Generic; + +namespace EventLogMonitor; + +[Collection("EventLogMonitor")] +public class EventLogMonitorTests +{ + [SuppressMessage("Microsoft.Usage", "IDE0052:RemoveUnreadPrivateMember", MessageId = "stdoutput")] + private readonly ITestOutputHelper stdoutput; + private readonly string ace11SampleEventLog; + private readonly string powerShellSampleEventLog; + private readonly string vSSSampleEventLog; + private readonly string restartManagerSampleEventLog; + private readonly string securitySampleEventLog; + private readonly string invalidEventLogName; + + public EventLogMonitorTests(ITestOutputHelper testOutputHelper) + { + stdoutput = testOutputHelper; + ace11SampleEventLog = "../../../../../test/EventLogMonitorTests/SampleEventLogs/ACE-11-Log.evtx"; + powerShellSampleEventLog = "../../../../../test/EventLogMonitorTests/SampleEventLogs/POSH-Log.evtx"; + vSSSampleEventLog = "../../../../../test/EventLogMonitorTests/SampleEventLogs/VSS-Log.evtx"; + restartManagerSampleEventLog = "../../../../../test/EventLogMonitorTests/SampleEventLogs/RestartMgr-Log.evtx"; + securitySampleEventLog = "../../../../../test/EventLogMonitorTests/SampleEventLogs/Security-Log.evtx"; + invalidEventLogName = "../../../../../test/EventLogMonitorTests/SampleEventLogs/Invalid-Log.evtx"; + } + + [Fact] + public void NoArgumentsPasedInIsAllowed() + { + string[] args = Array.Empty(); + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized); + } + + [Theory] + [InlineData("-?")] + [InlineData("-help")] + public void HelpIsReturned(string helpOption) + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { helpOption }; + EventLogMonitor monitor = new(); + monitor.Initialize(args); + string help = output.ToString(); + Assert.StartsWith("EventLogMonitor : Version", help); + Assert.EndsWith("When tailing the event log at the console, press , 'Q' or to exit.", help.TrimEnd()); + } + + [Fact] + public void VersionIsReturned() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-version" }; + EventLogMonitor monitor = new(); + monitor.Initialize(args); + string version = output.ToString(); + Assert.StartsWith("EventLogMonitor version 2.", version); + } + + [Fact] + public void InvalidArgumentGivesAnErrorMessage() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-imvalidArg" }; + EventLogMonitor monitor = new(); + monitor.Initialize(args); + string help = output.ToString(); + Assert.StartsWith("Invalid arguments or invalid argument combination.", help); + } + + [Theory] + [InlineData("")] + [InlineData("-1")] // -1 is the default output type if not present or overridden with -2 or -3 + public void OptionP2ReturnsTwoMostRecentPreviousEvents(string extraOptions) + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, extraOptions }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); // one extra closing test line is returned + // most recent 2 entries + Assert.Equal("BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully. [23/12/2021 11:58:12.195]", lines[0]); + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message. [23/12/2021 11:58:12.195]", lines[1]); + Assert.StartsWith("2 Entries shown from the", lines[2]); + } + + [Fact] + public void OptionP2ReturnsTwoMostRecentPreviousEventsWithMediumOutput() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-2" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, lines.Length); // one extra closing test line is returned + // most recent 2 entries + // entry one + Assert.Equal("BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully.", lines[0].TrimEnd()); + Assert.Equal("The integration node received an operational control message containing an instruction to start the deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') and successfully performed this action. [23/12/2021 11:58:12.195]", lines[1]); + // entry 2 + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message.", lines[2].TrimEnd()); + Assert.Equal("A command response will be sent to the integration node. [23/12/2021 11:58:12.195]", lines[3]); + // tail + Assert.StartsWith("2 Entries shown from the", lines[4]); + } + + [Fact] + public void OptionP2ReturnsTwoMostRecentPreviousEventsWithFullOutput() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-3" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(7, lines.Length); // one extra closing test line is returned + // most recent 2 entries + // entry one + Assert.Equal("BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully.", lines[0].TrimEnd()); + Assert.Equal("The integration node received an operational control message containing an instruction to start the deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') and successfully performed this action.", lines[1].TrimEnd()); + Assert.Equal("No user action required. [23/12/2021 11:58:12.195]", lines[2]); + // entry 2 + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message.", lines[3].TrimEnd()); + Assert.Equal("A command response will be sent to the integration node.", lines[4].TrimEnd()); + Assert.Equal("No user action required. [23/12/2021 11:58:12.195]", lines[5]); + // tail + Assert.StartsWith("2 Entries shown from the", lines[6]); + } + + [Fact] + public void UsingMoreThanOneOutputTypeReturnsAnError() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-3", "-2" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.False(initialized, $"{initialized} should be false"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + Assert.StartsWith("Invalid arguments or invalid argument combination: only one of options '-1', '-2' and '-3' may be specified.", logOut); + Assert.EndsWith("When tailing the event log at the console, press , 'Q' or to exit.", logOut.TrimEnd()); + } + + + [Fact] + public void OptionP2WithTFReturnsTwoMostRecentPreviousEventsWithTimestampFirst() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-tf" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); // one extra closing test line is returned + // most recent 2 entries + Assert.Equal("23/12/2021 11:58:12.195: BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully.", lines[0]); + Assert.Equal("23/12/2021 11:58:12.195: BIP2154I: ( MGK.main ) Integration server finished with Configuration message.", lines[1]); + Assert.StartsWith("2 Entries shown from the", lines[2]); + } + + [Fact] + public void OptionP2ReturnsTwoMostRecentPreviousEventsWithBinaryDataAsUnicode() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-b1" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, lines.Length); // one extra closing test line is returned + // most recent 2 entries + // entry 1 + Assert.Equal("BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully. [23/12/2021 11:58:12.195]", lines[0]); + Assert.Equal("8464 MGK.main C:\\ci\\product-build\\WMB\\src\\DataFlowEngine\\MessageServices\\ImbResource.cpp 3566 ImbResource::logDeployStatusComp. Index: 282277", lines[1]); + // entry 2 + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message. [23/12/2021 11:58:12.195]", lines[2]); + Assert.Equal("8464 MGK.main C:\\ci\\product-build\\WMB\\src\\DataFlowEngine\\MessageServices\\ImbDataFlowNotifications.cpp 525 ImbDataFlowNotifications::output. Index: 282278", lines[3]); + // tail + Assert.StartsWith("2 Entries shown from the", lines[4]); + } + + [Fact] + public void OptionP2ReturnsTwoMostRecentPreviousEventsWithBinaryDataAsHex() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-b2" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(76, lines.Length); // one extra closing test line is returned + // most recent 2 entries + // event 1 + Assert.Equal("BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully. [23/12/2021 11:58:12.195]", lines[0]); + Assert.Equal("Binary Data size: 256", lines[1]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[2]); + Assert.Equal("00000008: 38 00 34 00-36 00 34 00 8.4.6.4. 00340038 00340036", lines[3]); + Assert.Equal("00000256: 6D 00 70 00-00 00 00 00 m.p..... 0070006D 00000000", lines[34]); //skip some lines before this one + Assert.StartsWith("Index: 282277", lines[35]); + // event 2 + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message. [23/12/2021 11:58:12.195]", lines[36]); + Assert.Equal("Binary Data size: 280", lines[37]); + Assert.Equal("Count : 00 01 02 03-04 05 06 07 ASCII 00 04", lines[38]); + Assert.Equal("00000008: 38 00 34 00-36 00 34 00 8.4.6.4. 00340038 00340036", lines[39]); + Assert.Equal("00000280: 75 00 74 00-00 00 00 00 u.t..... 00740075 00000000", lines[73]); //skip some lines before this one + Assert.StartsWith("Index: 282278", lines[74]); + // tail + Assert.StartsWith("2 Entries shown from the", lines[75]); + } + + [Fact] + public void OptionP2ReturnsTwoMostRecentPreviousEventsWithExtraVerboseOutput() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-v" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, lines.Length); // one extra closing test line is returned + // most recent 2 entries + // event 1 + Assert.Equal("BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully. [23/12/2021 11:58:12.195]", lines[0]); + Assert.StartsWith("Machine: mgk-PC3. Log:", lines[1]); + Assert.EndsWith("Source: IBM App Connect Enterprise v110011.", lines[1].TrimEnd()); + // event 2 + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message. [23/12/2021 11:58:12.195]", lines[2]); + Assert.StartsWith("Machine: mgk-PC3. Log:", lines[1]); + Assert.EndsWith("Source: IBM App Connect Enterprise v110011.", lines[1].TrimEnd()); + // tail + Assert.StartsWith("2 Entries shown from the", lines[4]); + } + + [Fact] + public void OptionP2ReturnsTwoMostRecentPreviousEventsInGerman() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-c", "de" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); // one extra closing test line is returned + // most recent 2 entries + Assert.Equal("BIP2269I: ( MGK.main ) Die implementierte Ressource ''test'' (UUID=''test'', Typ=''MessageFlow'') wurde erfolgreich gestartet. [23/12/2021 11:58:12.195]", lines[0]); + Assert.Equal("BIP2154I: ( MGK.main ) Integrationsserver hat die Verarbeitung der Konfigurationsnachricht abgeschlossen. [23/12/2021 11:58:12.195]", lines[1]); + Assert.StartsWith("2 Entries shown from the", lines[2]); + } + + [Fact] + public void OptionInvalidCultureReturnsError() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-l", ace11SampleEventLog, "-c", "fake" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(2, lines.Length); // one extra closing test line is returned + // most recent 2 entries + Assert.Equal("Culture is not supported. fake is an invalid culture identifier. Defaulting to 'En-US'.", lines[0].TrimEnd()); + Assert.StartsWith("0 Entries shown from the", lines[1]); + } + + [Fact] + public void OptionP2ReturnsTwoMostRecentPreviousEventsInEnglishWithInvalidCulture() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "2", "-l", ace11SampleEventLog, "-c", "ABC" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); // one extra closing test line is returned + // most recent 2 entries + Assert.Equal("BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully. [23/12/2021 11:58:12.195]", lines[0]); + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message. [23/12/2021 11:58:12.195]", lines[1]); + Assert.StartsWith("2 Entries shown from the", lines[2]); + } + + [Fact] + public void OptionPStarReturnsAll64PreviousEvents() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + // stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(65, lines.Length); // one extra closing test line is returned + // oldest 2 entries + Assert.Equal("BIP2001I: ( MGK ) The IBM App Connect Enterprise service has started at version '110011'; process ID 7192. [18/11/2021 18:21:16.344]", lines[0]); + Assert.Equal("BIP3132I: ( MGK ) The HTTP Listener has started listening on port ''4414'' for ''RestAdmin http'' connections. [18/11/2021 18:21:26.571]", lines[1]); + // most recent 2 entries + Assert.Equal("BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully. [23/12/2021 11:58:12.195]", lines[62]); + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message. [23/12/2021 11:58:12.195]", lines[63]); + Assert.StartsWith("64 Entries shown from the", lines[64]); + } + + [Fact] + public void OptionIndexReturnsSingleEvent() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "282229", "-b1", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); // one event with one binary line and one extra closing test line is returned + Assert.Equal("BIP2011W: ( MGK ) The IBM App Connect Enterprise service has been shutdown, process ID '7108'. [23/12/2021 11:57:04.661]", lines[0].TrimEnd()); + Assert.Equal(@"5812 MGK.service C:\ci\product-build\WMB\src\AdminAgent\BipService\Win32\BipServiceMain.cpp 962 ImbControlService::serviceHandle. Index: 282229", lines[1]); + // tail + Assert.StartsWith("Matching entry found in the", lines[2]); + } + + [Fact] + public void OptionIndexRangeReturnsThreeEventsInclusive() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "282229-282259", "-b1", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(7, lines.Length); // one event with one binary line and one extra closing test line is returned + //event 1 + Assert.Equal("BIP2011W: ( MGK ) The IBM App Connect Enterprise service has been shutdown, process ID '7108'. [23/12/2021 11:57:04.661]", lines[0].TrimEnd()); + Assert.Equal(@"5812 MGK.service C:\ci\product-build\WMB\src\AdminAgent\BipService\Win32\BipServiceMain.cpp 962 ImbControlService::serviceHandle. Index: 282229", lines[1]); + //event 2 + Assert.Equal("BIP2001I: ( MGK ) The IBM App Connect Enterprise service has started at version '110011'; process ID 7100. [23/12/2021 11:57:51.90]", lines[2].TrimEnd()); + Assert.Equal(@"6784 MGK.service C:\ci\product-build\WMB\src\AdminAgent\BipService\Win32\BipServiceMain.cpp 641 ImbControlService::serviceMain. Index: 282237", lines[3]); + //event 3 + Assert.Equal("BIP3132I: ( MGK ) The HTTP Listener has started listening on port ''4414'' for ''RestAdmin http'' connections. [23/12/2021 11:58:00.33]", lines[4].TrimEnd()); + Assert.Equal(@"8816 MGK.agent C:\ci\product-build\WMB\src\bipBroker\NodejsLoggingHooks.cpp 113 ace-admin-server server.js.. Index: 282259", lines[5]); + + // tail + Assert.StartsWith("Found 3 Matching entries found in the", lines[6]); + } + + [Fact] + public void OptionIndexWithOptionP3WithSparseIndexReturnsThreeEventsAfterIndex() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "240568", "-p", "3", "-b1", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(7, lines.Length); // one event with one binary line and one extra closing test line is returned + //event 1 + Assert.Equal("BIP3132I: ( MGK ) The HTTP Listener has started listening on port ''4414'' for ''RestAdmin http'' connections. [18/11/2021 18:21:26.571]", lines[0].TrimEnd()); + Assert.Equal(@"9336 MGK.agent C:\ci\product-build\WMB\src\bipBroker\NodejsLoggingHooks.cpp 113 ace-admin-server server.js.. Index: 240568", lines[1]); + //event 2 + Assert.Equal("BIP2866I: ( MGK ) IBM App Connect Enterprise administration security is 'inactive'. [18/11/2021 18:21:26.571]", lines[2].TrimEnd()); + Assert.Equal(@"9068 MGK.agent C:\ci\product-build\WMB\src\AdminSec\AdminSecAuthManager.cpp 105 AdminSecAuthManager::init. Index: 240569", lines[3]); + //event 3 + Assert.Equal(@"BIP2208I: ( MGK.main ) Integration server (64) started: process '9980'; thread '9976'; additional information: integrationNodeName ''MGK'' (operation mode ''advanced''); integrationServerUUID ''00000000-0000-0000-0000-000000000000''; integrationServerLabel ''main''; queueManagerName ''''; trusted 'false'; userId ''SYSTEM''; migrationNeeded 'false'; integrationNodeUUID ''63e1e703-5274-4930-afe2-bccc27b87214''; filePath ''D:\ACE\11.0.0.11\server''; workPath ''C:\ProgramData\IBM\MQSI''; ICU Converter Path ''''; ordinality '1'. [18/11/2021 18:21:27.569]", lines[4].TrimEnd()); + Assert.Equal(@"9976 MGK.main C:\ci\product-build\WMB\src\DataFlowEngine\Main\ImbMain.cpp 1713 ImbMain::start. Index: 240570", lines[5]); + // tail + Assert.StartsWith("Found 3 Matching entries found in the", lines[6]); + } + + [Fact] + public void OptionIndexWithSmallIndexAndLargerPValueReturnsCorrectIndexInError() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "5", "-p", "6", "-b1", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Single(lines); // one event with one binary line and one extra closing test line is returned + // tail + Assert.StartsWith("No entries found with matching index 5 in the", lines[0]); + } + + [Fact] + public void OptionIndexWithOptionP3ReturnsThreeEventsEitherSideOfTheIndex() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "240584", "-p", "3", "-b1", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(15, lines.Length); // one event with one binary line and one extra closing test line is returned + //event 1 + Assert.Equal("BIP2153I: ( MGK.main ) About to ''Start'' an integration server. [18/11/2021 18:21:38.345]", lines[0].TrimEnd()); + Assert.Equal(@"9976 MGK.main C:\ci\product-build\WMB\src\DataFlowEngine\MessageServices\ImbDataFlowNotifications.cpp 508 ImbDataFlowNotifications::output. Index: 240581", lines[1]); + //event 2 + Assert.Equal("BIP2155I: ( MGK.main ) About to ''Initialize'' the deployed resource ''testApp'' of type ''Application''. [18/11/2021 18:21:38.370]", lines[2].TrimEnd()); + Assert.Equal(@"9976 MGK.main C:\ci\product-build\WMB\src\DataFlowEngine\MessageServices\ImbResource.cpp 3459 ImbResource::logDeployStatus. Index: 240582", lines[3]); + //event 3 + Assert.Equal(@"BIP2155I: ( MGK.main ) About to ''Start'' the deployed resource ''testApp'' of type ''Application''. [18/11/2021 18:21:38.689]", lines[4].TrimEnd()); + Assert.Equal(@"9976 MGK.main C:\ci\product-build\WMB\src\DataFlowEngine\MessageServices\ImbResource.cpp 3459 ImbResource::logDeployStatus. Index: 240583", lines[5]); + //event 4 + Assert.Equal(@"BIP3132I: ( MGK.main ) The HTTP Listener has started listening on port '7800' for ''http'' connections. [18/11/2021 18:21:38.801]", lines[6].TrimEnd()); + Assert.Equal(@"9976 MGK.main C:\ci\product-build\WMB\src\WebServices\EventHTTP\RequestQueue.cpp 437 Imb::EvHtp::RequestQueue::regist. Index: 240584", lines[7]); + //event 5 + Assert.Equal(@"BIP1996I: ( MGK.main ) Listening on HTTP URL ''/test''. [18/11/2021 18:21:38.802]", lines[8].TrimEnd()); + Assert.Equal(@"9976 MGK.main C:\ci\product-build\WMB\src\WebServices\WSLibrary\ImbWSInputNode.cpp 599 Imb::WSInputNode::onStart. Index: 240585", lines[9]); + //event 6 + Assert.Equal(@"BIP2269I: ( MGK.main ) Deployed resource ''test'' (uuid=''test'',type=''MessageFlow'') started successfully. [18/11/2021 18:21:38.805]", lines[10].TrimEnd()); + Assert.Equal(@"9976 MGK.main C:\ci\product-build\WMB\src\DataFlowEngine\MessageServices\ImbResource.cpp 3566 ImbResource::logDeployStatusComp. Index: 240586", lines[11]); + //event 7 + Assert.Equal(@"BIP2154I: ( MGK.main ) Integration server finished with Configuration message. [18/11/2021 18:21:38.806]", lines[12].TrimEnd()); + Assert.Equal(@"9976 MGK.main C:\ci\product-build\WMB\src\DataFlowEngine\MessageServices\ImbDataFlowNotifications.cpp 525 ImbDataFlowNotifications::output. Index: 240587", lines[13]); + // tail + Assert.StartsWith("Found 7 Matching entries found in the", lines[14]); + } + + [Fact] + public void OptionIndexWithOptionPStarReturnsLast8Events() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "282271", "-p", "*", "-b1", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(17, lines.Length); + //event 1 + Assert.Equal("BIP2152I: ( MGK.main ) Configuration message received. [23/12/2021 11:58:11.763]", lines[0].TrimEnd()); + Assert.Equal(@"8464 MGK.main C:\ci\product-build\WMB\src\DataFlowEngine\MessageServices\ImbDataFlowNotifications.cpp 504 ImbDataFlowNotifications::output. Index: 282271", lines[1]); + //event 2 + Assert.Equal("BIP2154I: ( MGK.main ) Integration server finished with Configuration message. [23/12/2021 11:58:12.195]", lines[14].TrimEnd()); + Assert.Equal(@"8464 MGK.main C:\ci\product-build\WMB\src\DataFlowEngine\MessageServices\ImbDataFlowNotifications.cpp 525 ImbDataFlowNotifications::output. Index: 282278", lines[15]); + // tail + Assert.StartsWith("Found 8 Matching entries found in the", lines[16]); + } + + [Fact] + public void InvalidIndexRangeReturnsError() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "282-229-345", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.False(initialized, $"{initialized} should be false"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + Assert.StartsWith("Invalid arguments or invalid argument combination: invalid range, use 'x-y' to specify a range.", logOut); + } + + [Fact] + public void UsingAnIndexWithOptionSIsAnError() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "229-282", "-s", "newSource", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.False(initialized, $"{initialized} should be false"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + Assert.StartsWith("Invalid arguments or invalid argument combination: -s not allowed with -i.", logOut); + } + + [Fact] + public void InvalidIndexRangeEndLessThanStartReturnsError() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "282271-282270", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.False(initialized, $"{initialized} should be false"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + Assert.StartsWith("Invalid arguments or invalid argument combination: index max > index min.", logOut); + } + + [Fact] + public void IndexHigherThanHigestEntryInLogIsOKReturnsNoEvents() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "282280", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Single(lines); + Assert.StartsWith("No entries found with matching index 282280 in the", lines[0]); + } + + [Fact] + public void OptionFilterIncludeReturns5EventsInEnglish() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-fi", "Listening", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(6, lines.Length); + //event 1 + Assert.Equal("BIP1996I: ( MGK.main ) Listening on HTTP URL ''/test''. [18/11/2021 18:21:38.802]", lines[0].TrimEnd()); + //event 5 + Assert.Equal("BIP1996I: ( MGK.main ) Listening on HTTP URL ''/test''. [23/12/2021 11:58:12.194]", lines[4].TrimEnd()); + // tail + Assert.StartsWith("5 Entries shown from the", lines[5]); + } + + [Fact] + public void OptionFilterIncludeReturns5EventsInGerman() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-fi", "empfangsbereit", "-c", "De-DE", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(6, lines.Length); + //event 1 + Assert.Equal("BIP1996I: ( MGK.main ) Unter HTTP-URL ''/test'' empfangsbereit. [18/11/2021 18:21:38.802]", lines[0].TrimEnd()); + //event 5 + Assert.Equal("BIP1996I: ( MGK.main ) Unter HTTP-URL ''/test'' empfangsbereit. [23/12/2021 11:58:12.194]", lines[4].TrimEnd()); + // tail + Assert.StartsWith("5 Entries shown from the", lines[5]); + } + + [Fact] + public void OptionFilterIncludeReturns4Entries() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-fi", "shutdown", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, lines.Length); + //event 1 + Assert.Equal("BIP3988I: ( MGK ) The integration node ''MGK'' is performing an immediate shutdown. [24/11/2021 18:52:43.396]", lines[0].TrimEnd()); + //event 4 + Assert.Equal("BIP2011W: ( MGK ) The IBM App Connect Enterprise service has been shutdown, process ID '7108'. [23/12/2021 11:57:04.661]", lines[3].TrimEnd()); + // tail + Assert.StartsWith("4 Entries shown from the", lines[4]); + } + + [Fact] + public void OptionFilterIncludeAndExcludeReturns2Entries() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-fi", "shutdown", "-fx", "immediate", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); + //event 1 + Assert.Equal("BIP2011W: ( MGK ) The IBM App Connect Enterprise service has been shutdown, process ID '7028'. [30/11/2021 22:49:47.66]", lines[0].TrimEnd()); + //event 2 + Assert.Equal("BIP2011W: ( MGK ) The IBM App Connect Enterprise service has been shutdown, process ID '7108'. [23/12/2021 11:57:04.661]", lines[1].TrimEnd()); + // tail + Assert.StartsWith("2 Entries shown from the", lines[2]); + } + + [Fact] + public void OptionFilterIncludeAndExcludeInfoReturns2Entries() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-fi", "shutdown", "-fw", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); + //event 1 + Assert.Equal("BIP2011W: ( MGK ) The IBM App Connect Enterprise service has been shutdown, process ID '7028'. [30/11/2021 22:49:47.66]", lines[0].TrimEnd()); + //event 2 + Assert.Equal("BIP2011W: ( MGK ) The IBM App Connect Enterprise service has been shutdown, process ID '7108'. [23/12/2021 11:57:04.661]", lines[1].TrimEnd()); + // tail + Assert.StartsWith("2 Entries shown from the", lines[2]); + } + + [Fact] + public void OptionFilterErrorOnlyReturns0Entries() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-fe", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Single(lines); + // tail + Assert.StartsWith("0 Entries shown from the", lines[0]); + } + + [Fact] + public void OptionDShowsDetailsOfEventLogFile() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-d", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(4, lines.Length); + Assert.Equal("LogName : Entries : LastWriteTime : [DisplayName]", lines[0]); + Assert.Equal("-------------------------------------------------", lines[1]); + Assert.EndsWith("ACE-11-Log.evtx : 64 : 30/12/2021 17:03:04 : [ACE-11-Log]", lines[2]); + Assert.Equal("1 Provider file listed.", lines[3]); + } + + [Fact] + public void OptionDShowsDetailsOfEventLogFileVerbose() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-d", "-v", "-l", ace11SampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(9, lines.Length); + Assert.EndsWith("ACE-11-Log.evtx", lines[0]); + Assert.Equal(" DisplayName: ACE-11-Log", lines[1]); + Assert.Equal(" Records: 64", lines[2]); + Assert.Equal(" FileSize: 69632", lines[3]); + Assert.Equal(" IsFull: False", lines[4]); + Assert.StartsWith(" CreationTime:", lines[5]); // this varies if the file is moved + Assert.Equal(" LastWrite: 30/12/2021 17:03:04", lines[6]); + Assert.Equal(" OldestIndex: 1", lines[7]); + Assert.Equal("1 Provider file listed.", lines[8]); + } + + [Fact] + public void OptionDAloneShowsDetailsOfEventLogs() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-d" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.InRange(lines.Length, 10, 1000000); // should be at least 10 returned + Assert.Equal("LogName : Entries : LastWriteTime : [DisplayName]", lines[0]); + Assert.Equal("-------------------------------------------------", lines[1]); + Assert.EndsWith("Providers listed.", lines[^1]); + } + + [Fact] + public void OptionDWithMultiOptionLReturnsMultipleEventLogs() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-d", "-l", "Application, Windows PowerShell" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.InRange(lines.Length, 5, 1000000); // should be at least 5 lines (3 boiler plate, 2 logs) returned + Assert.Equal("LogName : Entries : LastWriteTime : [DisplayName]", lines[0]); + Assert.Equal("-------------------------------------------------", lines[1]); + Assert.EndsWith("Providers listed.", lines[^1]); + // we cannot guarantee the line order returned so just do a scan check to make sure we got something + Assert.Contains("Application", logOut); + Assert.Contains("Windows PowerShell", logOut); + } + + [Fact] + public void OptionDWithSingleOptionLReturnsSingleEventLog() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-d", "-l", "Windows PowerShell" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.InRange(lines.Length, 4, 1000000); // should be at least 5 lines (3 boiler plate, 2 logs) returned + Assert.Equal("LogName : Entries : LastWriteTime : [DisplayName]", lines[0]); + Assert.Equal("-------------------------------------------------", lines[1]); + Assert.EndsWith("Providers listed.", lines[^1]); + // we cannot guarantee a single line returned so just do a scan check to make sure we got something + Assert.Contains("Windows PowerShell", logOut); + } + + [Fact] + public void OptionDShowsDetailsOfRealEventLogVerbose() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-d", "-v", "-l", "Windows Powershell" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + // we can only really check the shape without making this test too brittle + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(15, lines.Length); + Assert.EndsWith("Windows PowerShell", lines[0]); + Assert.Equal(" DisplayName: Windows PowerShell", lines[1]); + Assert.StartsWith(" Records:", lines[2]); + Assert.StartsWith(" FileSize:", lines[3]); + Assert.StartsWith(" IsFull:", lines[4]); + Assert.StartsWith(" CreationTime:", lines[5]); + Assert.StartsWith(" LastWrite:", lines[6]); + Assert.StartsWith(" OldestIndex:", lines[7]); + Assert.StartsWith(" IsClassic:", lines[8]); + Assert.StartsWith(" IsEnabled:", lines[9]); + Assert.StartsWith(" LogFile:", lines[10]); + Assert.StartsWith(" LogType:", lines[11]); + Assert.StartsWith(" MaxSizeBytes:", lines[12]); + Assert.StartsWith(" MaxBuffers:", lines[13]); + Assert.Equal("1 Providers listed.", lines[14]); + } + + [Fact] + public void EventLogReaderReturnsEvent() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + var mockEventRecord = new Mock(); + mockEventRecord.Setup(x => x.Id).Returns(42); + mockEventRecord.Setup(x => x.LogName).Returns("Applicationz"); + var mockQuery = new Mock("Application", PathType.LogName, null); + var mockReader = new Mock(mockQuery.Object); + + mockReader.Setup(x => x.ReadEvent()).Returns(mockEventRecord.Object); + + var testEvent = mockReader.Object.ReadEvent(); + Assert.Equal(42, testEvent.Id); + Assert.Equal("Applicationz", testEvent.LogName); + + } + [Fact] + public void NoneExistentMessageIDReturnsErrorMessage() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + // mock an event to raise + var mockEventRecord = new Mock(); + mockEventRecord.Setup(x => x.Id).Returns(42); + mockEventRecord.Setup(x => x.ProviderName).Returns("WebSphere Broker"); + mockEventRecord.Setup(x => x.Properties).Returns(new List()); + mockEventRecord.Setup(x => x.TimeCreated).Returns(new DateTime(2000, 1, 1, 12, 0, 0)); + var mockQuery = new Mock("Application", PathType.LogName, null); + var mockWatch = new Mock(mockQuery.Object); + + string[] monitorArgs = new string[] { "-nt" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(monitorArgs); + Assert.True(initialized, $"{initialized} should be true"); + var args = new Mock(); + args.Object.EventRecord = mockEventRecord.Object; + mockWatch.Object.EventRecordWritten += new EventHandler(monitor.EventLogEventRead); + mockWatch.Object.Enabled = true; + mockWatch.Raise(e => e.EventRecordWritten += null, this, args.Object); + + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Single(lines); + Assert.Equal("BIP42I: The description for Event ID 42 from source WebSphere Broker cannot be found. Either the component that raises this event is not installed on your local computer or the installation is corrupted. You can install or repair the component on the local computer. [01/01/2000 12:00:00.0]", lines[0]); + } + + [Fact] + public void OptionMultiSourceReturnsNoEvents() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-s", "A,B,C", "-l", "System", "-nt" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Single(lines); + Assert.Equal("0 Entries shown from the System log matching the event source 'A' or 'B' or 'C'.", lines[0]); + } + + [Fact] + public void OptionSingleSourceReturnsNoEvents() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-s", "Z", "-l", "System", "-nt" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Single(lines); + Assert.Equal("0 Entries shown from the System log matching the event source 'Z'.", lines[0]); + } + + [Fact] + public void OptionMultiSourceReturnsEvents() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + string[] args = new string[] { "-s", "Restart, Test, Power", "-l", powerShellSampleEventLog, "-p", "*" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(6, lines.Length); // one extra closing test line is returned + Assert.Equal("600I: Provider \"Variable\" is Started. [11/01/2022 18:37:18.339]", lines[0].TrimEnd()); + Assert.Equal("400I: Engine state is changed from None to Available. [11/01/2022 18:37:18.977]", lines[1]); + Assert.Equal("800I: Pipeline execution details for command line: Add-Type -TypeDefinition $Source -Language CSharp -IgnoreWarnings. [11/01/2022 18:37:22.771]", lines[2].TrimEnd()); + Assert.Equal("600I: Provider \"Certificate\" is Started. [11/01/2022 18:37:25.811]", lines[3]); + Assert.Equal("403I: Engine state is changed from Available to Stopped. [11/01/2022 18:37:26.805]", lines[4]); + // tail + Assert.StartsWith("5 Entries shown from the", lines[5]); + Assert.EndsWith("POSH-Log.evtx log matching the event source 'Restart' or 'Test' or 'Power'.", lines[5]); + } + + [Fact] + public void CliOptionsReturnsZeroEntriesTailing() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + // replace stdin to fake it + string esc = ((char)0x1b).ToString(); // Dec 27 + string inputData = "sssx" + esc; // 3 's' for stats, one 'x' to make sure it is ignored and an 'ESC' to quit + var input = new StringReader(inputData); + Console.SetIn(input); + Environment.SetEnvironmentVariable("EVENTLOGMONITOR_INPUT_REDIRECTED", "true"); + + string[] args = new string[] { "-s", "FakeSource", "-l", "System" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, lines.Length); + Assert.Equal("Waiting for events from the System log matching the event source 'FakeSource'.", lines[0]); + Assert.Equal("0 Entries shown so far from the System log. Waiting for more events...", lines[1]); // from the "s" option passed in + Assert.Equal("0 Entries shown so far from the System log. Waiting for more events...", lines[2]); // from the "s" option passed in + Assert.Equal("0 Entries shown so far from the System log. Waiting for more events...", lines[3]); // from the "s" option passed in + Assert.Equal("0 Entries shown from the System log matching the event source 'FakeSource'.", lines[4]); + Console.In.Close(); + } + + [Fact] + public void CliOptionEnterWillQuitTailing() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + // replace stdin to fake it + string inputData = ((char)0x0D).ToString(); //Dec 13 + var input = new StringReader(inputData); + Console.SetIn(input); + Environment.SetEnvironmentVariable("EVENTLOGMONITOR_INPUT_REDIRECTED", "true"); + + string[] args = new string[] { "-s", "FakeSource", "-l", "System" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(2, lines.Length); + Assert.Equal("Waiting for events from the System log matching the event source 'FakeSource'.", lines[0]); + Assert.Equal("0 Entries shown from the System log matching the event source 'FakeSource'.", lines[1]); + Console.In.Close(); + } + + [Fact] + public void CliOptionQWillQuitTailing() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + // replace stdin to fake it + string inputData = "q"; + var input = new StringReader(inputData); + Console.SetIn(input); + Environment.SetEnvironmentVariable("EVENTLOGMONITOR_INPUT_REDIRECTED", "true"); + + string[] args = new string[] { "-s", "FakeSource", "-l", "System" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(2, lines.Length); + Assert.Equal("Waiting for events from the System log matching the event source 'FakeSource'.", lines[0]); + Assert.Equal("0 Entries shown from the System log matching the event source 'FakeSource'.", lines[1]); + Console.In.Close(); + } + + [Fact] + public void CliOptionEOSWillQuitTailing() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + // replace stdin to fake it + string inputData = ""; // this will give an EOS when it is read + var input = new StringReader(inputData); + Console.SetIn(input); + Environment.SetEnvironmentVariable("EVENTLOGMONITOR_INPUT_REDIRECTED", "true"); + + string[] args = new string[] { "-s", "FakeSource", "-l", "System" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(2, lines.Length); + Assert.Equal("Waiting for events from the System log matching the event source 'FakeSource'.", lines[0]); + Assert.Equal("0 Entries shown from the System log matching the event source 'FakeSource'.", lines[1]); + Console.In.Close(); + } + + [Fact] + public void OptionInvalidIndexReturnsNoEvents() + { + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "4294967295" }; // choose an index unlikely to be ever used (uint max) + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Single(lines); + Assert.Equal("No entries found with matching index 4294967295 in the Application log", lines[0]); + Console.In.Close(); + } + + [Fact] + public void PowerShellLogReturnsSingleLinesOutput() + { + // The "Windows PowerShell" log has an embedded '\r\n' in the event 800 which should be stripped out + + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-l", powerShellSampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(6, lines.Length); // one extra closing test line is returned + Assert.Equal("600I: Provider \"Variable\" is Started. [11/01/2022 18:37:18.339]", lines[0].TrimEnd()); + Assert.Equal("400I: Engine state is changed from None to Available. [11/01/2022 18:37:18.977]", lines[1]); + Assert.Equal("800I: Pipeline execution details for command line: Add-Type -TypeDefinition $Source -Language CSharp -IgnoreWarnings. [11/01/2022 18:37:22.771]", lines[2].TrimEnd()); + Assert.Equal("600I: Provider \"Certificate\" is Started. [11/01/2022 18:37:25.811]", lines[3]); + Assert.Equal("403I: Engine state is changed from Available to Stopped. [11/01/2022 18:37:26.805]", lines[4]); + // tail + Assert.StartsWith("5 Entries shown from the", lines[5]); + } + + [Fact] + public void VSSLogReturnsASCIIOutputOutput() + { + // The "VSS" log has an 2 error messages with ASCII binary data in it which should be automatically be shown + + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-l", vSSSampleEventLog, "-s", "*" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(7, lines.Length); // one extra closing test line is returned + Assert.Equal("8224I: The VSS service is shutting down due to idle timeout. [21/12/2021 21:49:36.851]", lines[0].TrimEnd()); + Assert.Equal("8193E: Volume Shadow Copy Service error: Unexpected error calling routine QueryFullProcessImageNameW. hr = 0x8007001f, A device attached to the system is not functioning. [30/12/2021 10:53:12.512]", lines[1]); + Assert.Equal("- Code: SECSECRC00000581- Call: SECSECRC00000565- PID: 00032356- TID: 00000788- CMD: C:\\WINDOWS\\system32\\vssvc.exe - User: Name: NT AUTHORITY\\SYSTEM, SID:S-1-5-18. Index: 290107", lines[2].TrimEnd()); + Assert.Equal("8193E: Volume Shadow Copy Service error: Unexpected error calling routine QueryFullProcessImageNameW. hr = 0x8007001f, A device attached to the system is not functioning. [30/12/2021 10:55:38.380]", lines[3]); + Assert.Equal("- Code: SECSECRC00000581- Call: SECSECRC00000565- PID: 00032356- TID: 00028688- CMD: C:\\WINDOWS\\system32\\vssvc.exe - User: Name: NT AUTHORITY\\SYSTEM, SID:S-1-5-18. Index: 290277", lines[4]); + Assert.Equal("8224I: The VSS service is shutting down due to idle timeout. [30/12/2021 11:03:26.902]", lines[5].TrimEnd()); + // tail + Assert.StartsWith("4 Entries shown from the", lines[6]); + } + + [Fact] + public void VSSLogReturnsCorrectOutputWithOption2() + { + // The "VSS" log has error messages that use '\r\n' rather than '\r\n\rn' and this checks we handle that case + + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-i", "290107", "-l", vSSSampleEventLog, "-2" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(8, lines.Length); // one extra closing test line is returned + Assert.Equal("8193E: Volume Shadow Copy Service error: Unexpected error calling routine QueryFullProcessImageNameW. hr = 0x8007001f, A device attached to the system is not functioning.", lines[0].TrimEnd()); + Assert.Equal(".", lines[1].TrimEnd()); + Assert.Equal("Operation:", lines[2].TrimEnd()); + Assert.Equal(" Executing Asynchronous Operation", lines[3]); + Assert.Equal("Context:", lines[4]); + Assert.Equal(" Current State: DoSnapshotSet [30/12/2021 10:53:12.512]", lines[5].TrimEnd()); + Assert.Equal(@"- Code: SECSECRC00000581- Call: SECSECRC00000565- PID: 00032356- TID: 00000788- CMD: C:\WINDOWS\system32\vssvc.exe - User: Name: NT AUTHORITY\SYSTEM, SID:S-1-5-18. Index: 290107", lines[6].TrimEnd()); + // tail + Assert.StartsWith("Matching entry found in the", lines[7]); + } + + [Fact] + public void InvalidLogReturnsErrorWhenUsed() + { + // The "Invalid-Log" log has a valid message from the VSS service in it but the FormatMessage API can't parse it and we produce an error + + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-l", invalidEventLogName, "-3" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, lines.Length); // one extra closing test line is returned + Assert.Equal("8224I: The description for Event ID 8224 from source VSS cannot be found. Either the component that raises this event is not installed on your local computer or the installation is corrupted. You can install or repair the component on the local computer.", lines[0].TrimEnd()); + Assert.Equal("If the event originated on another computer, the display information had to be saved with the event.", lines[1]); + Assert.Equal("The following information was included with the event:", lines[2].TrimEnd()); + Assert.Equal("The message resource is present but the message was not found in the message table [21/12/2021 21:49:36.851]", lines[3].TrimEnd()); + // tail + Assert.StartsWith("1 Entries shown from the", lines[4]); + } + + [Fact] + public void RestartManagerLogReturnsUserIdWhenVerboseOptionSet() + { + // The RestartManager log contains an example of a user id, process id and thread id + + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-l", restartManagerSampleEventLog, "-v" }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); // one extra closing test line is returned + Assert.Equal("10005I: Machine restart is required. [16/01/2022 14:59:02.873]", lines[0].TrimEnd()); + Assert.StartsWith("Machine: mgk-PC3. Log: ", lines[1]); + Assert.EndsWith("EventLogMonitor\\test\\EventLogMonitorTests\\SampleEventLogs\\RestartMgr-Log.evtx. Source: Microsoft-Windows-RestartManager. User: S-1-5-18. ProcessId: 44120. ThreadId: 40204.", lines[1]); + // tail + Assert.StartsWith("1 Entries shown from the", lines[2]); + } + + [Fact] + public void SecurityLogReturnsAFAndASSuffixedEventIds() + { + // The Security log should be formattable on any machine with En-US installed + + // replace stdout to capture it + var output = new StringWriter(); + Console.SetOut(output); + + string[] args = new string[] { "-p", "*", "-l", securitySampleEventLog }; + EventLogMonitor monitor = new(); + bool initialized = monitor.Initialize(args); + Assert.True(initialized, $"{initialized} should be true"); + monitor.MonitorEventLog(); + string logOut = output.ToString(); + stdoutput.WriteLine(logOut); + string[] lines = logOut.Split(new string[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, lines.Length); // one extra closing test line is returned + Assert.Equal("4957F: Windows Firewall did not apply the following rule: [05/02/2022 13:54:04.551]", lines[0].TrimEnd()); + Assert.StartsWith("4662S: An operation was performed on an object. [05/02/2022 14:40:21.712]", lines[1]); + Assert.EndsWith("4662S: An operation was performed on an object. [05/02/2022 14:40:21.720]", lines[2]); + Assert.EndsWith("4957F: Windows Firewall did not apply the following rule: [06/02/2022 15:57:02.961]", lines[3]); + // tail + Assert.StartsWith("4 Entries shown from the", lines[4]); + } +} + +public abstract class TestEventLogReader : EventLogReader +{ + public TestEventLogReader() : base("") { } + public TestEventLogReader(EventLogQuery a) : base(a) { } + + public abstract new EventRecord ReadEvent(); +} + +public abstract class TestEventLogWatcher : EventLogWatcher +{ + public TestEventLogWatcher() : base("") { } + public TestEventLogWatcher(EventLogQuery a) : base(a) { } + public abstract new event EventHandler EventRecordWritten; +} + diff --git a/test/EventLogMonitorTests/EventLogMonitorTests.csproj b/test/EventLogMonitorTests/EventLogMonitorTests.csproj new file mode 100644 index 0000000..170e68f --- /dev/null +++ b/test/EventLogMonitorTests/EventLogMonitorTests.csproj @@ -0,0 +1,39 @@ + + + + net6.0-windows + enable + + false + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file diff --git a/test/EventLogMonitorTests/SampleEventLogs/ACE-11-Log.dll b/test/EventLogMonitorTests/SampleEventLogs/ACE-11-Log.dll new file mode 100644 index 0000000..77ea82c Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/ACE-11-Log.dll differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/ACE-11-Log.evtx b/test/EventLogMonitorTests/SampleEventLogs/ACE-11-Log.evtx new file mode 100644 index 0000000..6feafb9 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/ACE-11-Log.evtx differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/Invalid-Log.evtx b/test/EventLogMonitorTests/SampleEventLogs/Invalid-Log.evtx new file mode 100644 index 0000000..830c102 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/Invalid-Log.evtx differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/InvalidMessageCatalogue.dll b/test/EventLogMonitorTests/SampleEventLogs/InvalidMessageCatalogue.dll new file mode 100644 index 0000000..9fcc244 --- /dev/null +++ b/test/EventLogMonitorTests/SampleEventLogs/InvalidMessageCatalogue.dll @@ -0,0 +1,2 @@ +This is not a dll. +This is here to represent an invalid dll. \ No newline at end of file diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_1028.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_1028.MTA new file mode 100644 index 0000000..581d279 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_1028.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_2052.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_2052.MTA new file mode 100644 index 0000000..cbf0a88 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_2052.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_3076.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_3076.MTA new file mode 100644 index 0000000..eee894b Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_3076.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_4100.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_4100.MTA new file mode 100644 index 0000000..eee894b Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_4100.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_5124.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_5124.MTA new file mode 100644 index 0000000..02a66be Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/ACE-11-Log_5124.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Invalid-Log_2057.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Invalid-Log_2057.MTA new file mode 100644 index 0000000..9967405 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Invalid-Log_2057.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_1031.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_1031.MTA new file mode 100644 index 0000000..52b2d39 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_1031.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_1033.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_1033.MTA new file mode 100644 index 0000000..67e444e Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_1033.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_2057.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_2057.MTA new file mode 100644 index 0000000..20604ce Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/PowerShell_2057.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_1031.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_1031.MTA new file mode 100644 index 0000000..29e7141 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_1031.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_1033.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_1033.MTA new file mode 100644 index 0000000..0ff7c5a Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_1033.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_2057.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_2057.MTA new file mode 100644 index 0000000..0ff7c5a Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/RestartManager-Log_2057.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Security-Log_1033.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Security-Log_1033.MTA new file mode 100644 index 0000000..b24283d Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Security-Log_1033.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Security-Log_2057.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Security-Log_2057.MTA new file mode 100644 index 0000000..b24283d Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/Security-Log_2057.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_1031.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_1031.MTA new file mode 100644 index 0000000..8ac060b Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_1031.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_1033.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_1033.MTA new file mode 100644 index 0000000..f2943c4 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_1033.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_2057.MTA b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_2057.MTA new file mode 100644 index 0000000..87312fc Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/LocaleMetaData/VSS_2057.MTA differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/POSH-Log.dll b/test/EventLogMonitorTests/SampleEventLogs/POSH-Log.dll new file mode 100644 index 0000000..fae01eb Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/POSH-Log.dll differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/POSH-Log.evtx b/test/EventLogMonitorTests/SampleEventLogs/POSH-Log.evtx new file mode 100644 index 0000000..a1f6668 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/POSH-Log.evtx differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/RestartMgr-Log.dll b/test/EventLogMonitorTests/SampleEventLogs/RestartMgr-Log.dll new file mode 100644 index 0000000..c26fd4f Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/RestartMgr-Log.dll differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/RestartMgr-Log.evtx b/test/EventLogMonitorTests/SampleEventLogs/RestartMgr-Log.evtx new file mode 100644 index 0000000..ad4f4e0 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/RestartMgr-Log.evtx differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/Security-Log.evtx b/test/EventLogMonitorTests/SampleEventLogs/Security-Log.evtx new file mode 100644 index 0000000..37e1bdf Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/Security-Log.evtx differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/VSS-Log.dll b/test/EventLogMonitorTests/SampleEventLogs/VSS-Log.dll new file mode 100644 index 0000000..72fa0b9 Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/VSS-Log.dll differ diff --git a/test/EventLogMonitorTests/SampleEventLogs/VSS-Log.evtx b/test/EventLogMonitorTests/SampleEventLogs/VSS-Log.evtx new file mode 100644 index 0000000..1068bba Binary files /dev/null and b/test/EventLogMonitorTests/SampleEventLogs/VSS-Log.evtx differ