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