From d299c9f269740f03ab546c407e949245815146ec Mon Sep 17 00:00:00 2001 From: Christian Haase Date: Fri, 26 Dec 2025 16:07:10 +0100 Subject: [PATCH] Initial release --- Directory.Version.props | 2 +- README.md | 144 +++- .../Builders/EventLabelBuilder.cs | 26 + .../ByteGuard.SecurityLogger.csproj | 12 +- .../Configuration/ConfigurationValidator.cs | 26 + .../SecurityLoggerConfiguration.cs | 17 + .../Enrichers/PropertyEnricher.cs | 47 ++ .../LoggingVocabulary.cs | 66 ++ .../Partials/SecurityLogger.Authn.cs | 465 +++++++++++++ .../Partials/SecurityLogger.Authz.cs | 124 ++++ .../Partials/SecurityLogger.Crypt.cs | 75 ++ .../Partials/SecurityLogger.Excess.cs | 46 ++ .../Partials/SecurityLogger.Input.cs | 87 +++ .../Partials/SecurityLogger.Malicious.cs | 212 ++++++ .../Partials/SecurityLogger.Privilege.cs | 54 ++ .../Partials/SecurityLogger.Sensitive.cs | 157 +++++ .../Partials/SecurityLogger.Sequence.cs | 48 ++ .../Partials/SecurityLogger.Session.cs | 145 ++++ .../Partials/SecurityLogger.System.cs | 215 ++++++ .../Partials/SecurityLogger.Upload.cs | 177 +++++ .../Partials/SecurityLogger.User.cs | 173 +++++ .../SecurityEventMetadata.cs | 57 ++ .../SecurityLogger.cs | 50 ++ .../SecurityLoggerFactoryExtensions.cs | 18 + .../Builders/EventLabelBuilderTests.cs | 31 + ...ByteGuard.SecurityLogger.Tests.Unit.csproj | 9 +- .../ConfigurationValidatorTests.cs | 23 + .../Helpers/FakeLoggerExtensions.cs | 17 + .../Partials/AuthnTests.cs | 652 ++++++++++++++++++ .../Partials/AuthzTests.cs | 171 +++++ .../Partials/CryptTests.cs | 113 +++ .../Partials/ExcessTests.cs | 63 ++ .../Partials/InputTests.cs | 119 ++++ .../Partials/MaliciousTests.cs | 285 ++++++++ .../Partials/PrivilegeTests.cs | 67 ++ .../Partials/SensitiveTests.cs | 225 ++++++ .../Partials/SequenceTests.cs | 61 ++ .../Partials/SessionTests.cs | 219 ++++++ .../Partials/SystemTests.cs | 325 +++++++++ .../Partials/UploadTests.cs | 235 +++++++ .../Partials/UserTests.cs | 249 +++++++ .../SecurityLoggerFactoryExtensions.cs | 41 ++ .../SecurityLoggerTests.cs | 172 +++++ 43 files changed, 5515 insertions(+), 5 deletions(-) create mode 100644 src/ByteGuard.SecurityLogger/Builders/EventLabelBuilder.cs create mode 100644 src/ByteGuard.SecurityLogger/Configuration/ConfigurationValidator.cs create mode 100644 src/ByteGuard.SecurityLogger/Configuration/SecurityLoggerConfiguration.cs create mode 100644 src/ByteGuard.SecurityLogger/Enrichers/PropertyEnricher.cs create mode 100644 src/ByteGuard.SecurityLogger/LoggingVocabulary.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Authn.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Authz.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Crypt.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Excess.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Input.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Malicious.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Privilege.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Sensitive.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Sequence.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Session.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.System.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Upload.cs create mode 100644 src/ByteGuard.SecurityLogger/Partials/SecurityLogger.User.cs create mode 100644 src/ByteGuard.SecurityLogger/SecurityEventMetadata.cs create mode 100644 src/ByteGuard.SecurityLogger/SecurityLogger.cs create mode 100644 src/ByteGuard.SecurityLogger/SecurityLoggerFactoryExtensions.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Builders/EventLabelBuilderTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/ConfigurationValidatorTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Helpers/FakeLoggerExtensions.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/AuthnTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/AuthzTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/CryptTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/ExcessTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/InputTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/MaliciousTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/PrivilegeTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SensitiveTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SequenceTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SessionTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SystemTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/UploadTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/UserTests.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/SecurityLoggerFactoryExtensions.cs create mode 100644 tests/ByteGuard.SecurityLogger.Tests.Unit/SecurityLoggerTests.cs diff --git a/Directory.Version.props b/Directory.Version.props index 454e3ba..581c1f4 100644 --- a/Directory.Version.props +++ b/Directory.Version.props @@ -1,5 +1,5 @@ - 0.1.0 + 1.0.0 \ No newline at end of file diff --git a/README.md b/README.md index 23aba27..ab0084d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,147 @@ # ByteGuard.SecurityLogger ![NuGet Version](https://img.shields.io/nuget/v/ByteGuard.SecurityLogger) -`ByteGuard.SecurityLogger` brings the [OWASP Logging Vocabulary](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html) to .NET by exposing a set of strongly-typed `ILogger` extension methods for common security and audit events. +`ByteGuard.SecurityLogger` is a lightweight `ILogger` wrapper that brings the [OWASP Logging Vocabulary](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html) to .NET by exposing a set of strongly-typed `ILogger` methods for common security and audit events. Instead of ad-hoc log messages like `"login ok"` or `"unauthorized"`, you log standardized, structured events (e.g. `authn_login_success`, `authz_fail`) with consistent property names. This makes your security logs easier to search, alert on, correlate, and reason about, regardless of whether you send logs to Serilog, NLog, Application Insights, Elasticsearch, or something else. + +This package is **provider-agnostic**: it logs through `Microsoft.Extensions.Logging` so you can keep using your existing logging stack (Serilog, NLog, Application Insights, Seq, etc.) via normal logging providers. + +## Features + +- ✅ OWASP-aligned security event vocabulary +- ✅ Structured logging via `ILogger` scopes/properties +- ✅ Works with any `Microsoft.Extensions.ILogger` provider (_NLog, Serilog, etc._) + +## Getting Started + +### Installation + +This package is published and installed via [NuGet](https://www.nuget.org/packages/ByteGuard.SecurityLogger). + +Reference the package in your project: + +```bash +dotnet add package ByteGuard.SecurityLogger +``` + +## Usage + +Instantiate a new `SecurityLogger` instance using either the constructor or the `ILogger` extensions: `AsSecurityLogger()`. + +```csharp +ILogger logger = /* resolve or create ILogger */ + +var configuration = new SecurityLoggerConfiguration +{ + AppId = "MyApp" +} + +// Using constructor +var securityLogger = new SecurityLogger(logger, configuration); + +// Using ILogger extensions +var securityLogger = logger.AsSecurityLogger(configuration); +``` + +Log your security events: + +```csharp +var user = //... + +securityLogger.AuthnLoginSuccess( + "User {UserId} successfully logged in.", + userId: user.Id, + args: user.Id +) +``` + +## API Design + +`ByteGuard.SecurityLogger` implements the **full OWASP Logging Vocabulary**: every event type defined by OWASP exists as a corresponding method on `SecurityLogger`. + +### One method per event (plus an overload with metadata) + +For each OWASP event type, `SecurityLogger` exposes two overloads: + +1. A minimal overload for logging the event with just the event label parameters. + +```csharp +securityLogger.Log{event}( + string message, + /* event label arguments (varies by event) */, + params object?[] args +) +``` + +2. An overload that additionally accepts a `SecurityEventMetadata` object for richer, OWASP-recommended context (_client IP, hostname, request URI, etc._). + +```csharp +securityLogger.Log{event}( + string message, + /* event label arguments (varies by event) */, + SecurityEventMetadata metadata, + params object?[] args +) +``` + +### Parameter order (always the same) + +| Parameter | Description | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `message` | The human readable log message (_typically a message template_) | +| Event label arguments | These are the values required to form the OWASP event label, e.g.: `authn_login_success:{userId}` and `authz_fail:{userId,resource}` (_depending on the even type_). These are all nullable and will not be present in the label if provided as `null`. | +| `metadata` | Additional structured context recommended by OWASP (source IP, host, request URI, etc.) (_Only in the metadata method overload_) | +| `args` | The message template arguments from the 1st parameters | + +### Example + +If an event label requires a `userId`, the call becomes: + +```csharp +// Providing user ID produces label: authn_login_success:userOne +var userId = "userOne"; +securityLogger.LogAuthnLoginSuccess( + "User {UserId} logged in successfully from {Ip}", // Message template + userId, // Label parameters + userId, ip); // Template args + +// Without providing user ID produces label: authn_login_success +securityLogger.LogAuthnLoginSuccess( + "User {UserId} logged in successfully from {Ip}", // Message template + null, // Label parameters + userId, ip); // Template args +``` + +If you want to add OWASP-style context, use the metadata overload: + +```csharp +securityLogger.LogAuthnLoginSuccess( + "User {UserId} logged in successfully from {Ip}", // Message template + userId, // Label parameters + new SecurityEventMetadata // Event metadata + { + SourceIp = ip, + Hostname = host, + RequestUri = requestUri + }, + userId, ip); // Template args +``` + +> ℹ️ **Note:** The exact label arguments vary per event type, based on the OWASP Logging Vocabulary definition. + +## Configuration + +The `SecurityLogger` supports the following configurations: + +| Configuration | Required | Default | Description | +| ------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `AppId` | Yes | N/A | Application identifier added to the log message, to ensure logs are easy to find for the given application | +| `DisableSourceIpLogging` | No | `true` | Whether to log the `SourceIp` if provided (_logging user IP address may be useful for detection and response, but may be considered personally identifiable information when combined with other data and subject to regulation or deletion requests_) | + +## Supported events + +All supported events can be seen in the [WIKI](https://github.com/ByteGuard-HQ/byteguard-security-logger/wiki/Supported-events) + +## License + +_ByteGuard.SecurityLogger is Copyright © ByteGuard Contributors - Provided under the MIT license._ diff --git a/src/ByteGuard.SecurityLogger/Builders/EventLabelBuilder.cs b/src/ByteGuard.SecurityLogger/Builders/EventLabelBuilder.cs new file mode 100644 index 0000000..83500fe --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Builders/EventLabelBuilder.cs @@ -0,0 +1,26 @@ +namespace ByteGuard.SecurityLogger; + +/// +/// Combines events and event arguments into event strings. +/// +public static class EventLabelBuilder +{ + /// + /// Build an event string from the given event name and event arguments. + /// + /// Event name. + /// Event arguments. + /// An appropriate event string. + public static string BuildEventString(string eventName, params string?[] eventArgs) + { + if (eventArgs is null || eventArgs.Length == 0) + return eventName; + + var commaSeparatedEventArgs = string.Join(",", eventArgs.Where(arg => !string.IsNullOrEmpty(arg))); + + if (string.IsNullOrWhiteSpace(commaSeparatedEventArgs)) + return eventName; + + return $"{eventName}:{commaSeparatedEventArgs}"; + } +} diff --git a/src/ByteGuard.SecurityLogger/ByteGuard.SecurityLogger.csproj b/src/ByteGuard.SecurityLogger/ByteGuard.SecurityLogger.csproj index d963f06..692375e 100644 --- a/src/ByteGuard.SecurityLogger/ByteGuard.SecurityLogger.csproj +++ b/src/ByteGuard.SecurityLogger/ByteGuard.SecurityLogger.csproj @@ -1,9 +1,9 @@  - net8.0;net9.0;net10.0 + netstandard2.0 ByteGuard Contributors, detilium - OWASP Logging Vocabulary for .NET — ILogger extension methods that emit consistent, structured security events across any logging provider (Serilog, NLog, etc.).. + OWASP Logging Vocabulary for .NET brings a lightweight ILogger wrapper that emits consistent, structured security events across any logging provider (NLog, Serilog, etc.). https://github.com/ByteGuard-HQ/byteguard-security-logger https://github.com/ByteGuard-HQ/byteguard-security-logger git @@ -19,4 +19,12 @@ + + + + + + + + diff --git a/src/ByteGuard.SecurityLogger/Configuration/ConfigurationValidator.cs b/src/ByteGuard.SecurityLogger/Configuration/ConfigurationValidator.cs new file mode 100644 index 0000000..7641457 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Configuration/ConfigurationValidator.cs @@ -0,0 +1,26 @@ +namespace ByteGuard.SecurityLogger.Configuration; + +/// +/// Class used to validate a given security logger configuration instance. +/// +public static class ConfigurationValidator +{ + /// + /// Validate configuration and throw exceptions if invalid. + /// + /// Configuration instance to validate. + /// Throw if any required objects on the configuration object is null, or if the configuration object itself is null. + /// Thrown if any of the configuration values are invalid. + public static void ThrowIfInvalid(SecurityLoggerConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration), "Configuration cannot be null."); + } + + if (string.IsNullOrWhiteSpace(configuration.AppId)) + { + throw new ArgumentException("AppId cannot be null or empty.", nameof(configuration.AppId)); + } + } +} diff --git a/src/ByteGuard.SecurityLogger/Configuration/SecurityLoggerConfiguration.cs b/src/ByteGuard.SecurityLogger/Configuration/SecurityLoggerConfiguration.cs new file mode 100644 index 0000000..96a0f6f --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Configuration/SecurityLoggerConfiguration.cs @@ -0,0 +1,17 @@ +namespace ByteGuard.SecurityLogger.Configuration; + +/// +/// Configuration for the security logger. +/// +public class SecurityLoggerConfiguration +{ + /// + /// App identifier. + /// + public string AppId { get; set; } = default!; + + /// + /// Whether to disable logging of source IP addresses. Defaults to true. + /// + public bool DisableSourceIpLogging { get; set; } = true; +} diff --git a/src/ByteGuard.SecurityLogger/Enrichers/PropertyEnricher.cs b/src/ByteGuard.SecurityLogger/Enrichers/PropertyEnricher.cs new file mode 100644 index 0000000..cc475b1 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Enrichers/PropertyEnricher.cs @@ -0,0 +1,47 @@ +using ByteGuard.SecurityLogger.Configuration; + +namespace ByteGuard.SecurityLogger.Enrichers; + +internal static class PropertiesEnricher +{ + /// + /// Populate the properties from the given metadata instance. + /// + /// Properties to populate. + /// Metadata instance. + /// Security logger configuration. + internal static void PopulatePropertiesFromMetadata(Dictionary properties, SecurityEventMetadata? metadata, SecurityLoggerConfiguration configuration) + { + if (metadata is null) return; + + if (!string.IsNullOrWhiteSpace(metadata.UserAgent)) + properties.Add("UserAgent", metadata.UserAgent); + + if (!string.IsNullOrWhiteSpace(metadata.SourceIp) && !configuration.DisableSourceIpLogging) + properties.Add("SourceIp", metadata.SourceIp); + + if (!string.IsNullOrWhiteSpace(metadata.HostIp)) + properties.Add("HostIp", metadata.HostIp); + + if (!string.IsNullOrWhiteSpace(metadata.Hostname)) + properties.Add("Hostname", metadata.Hostname); + + if (!string.IsNullOrWhiteSpace(metadata.Protocol)) + properties.Add("Protocol", metadata.Protocol); + + if (!string.IsNullOrWhiteSpace(metadata.Port)) + properties.Add("Port", metadata.Port); + + if (!string.IsNullOrWhiteSpace(metadata.RequestUri)) + properties.Add("RequestUri", metadata.RequestUri); + + if (!string.IsNullOrWhiteSpace(metadata.RequestMethod)) + properties.Add("RequestMethod", metadata.RequestMethod); + + if (!string.IsNullOrWhiteSpace(metadata.Region)) + properties.Add("Region", metadata.Region); + + if (!string.IsNullOrWhiteSpace(metadata.Geo)) + properties.Add("Geo", metadata.Geo); + } +} diff --git a/src/ByteGuard.SecurityLogger/LoggingVocabulary.cs b/src/ByteGuard.SecurityLogger/LoggingVocabulary.cs new file mode 100644 index 0000000..904d4e7 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/LoggingVocabulary.cs @@ -0,0 +1,66 @@ +namespace ByteGuard.SecurityLogger; + +internal static class LoggingVocabulary +{ + internal const string AuthnLoginSuccess = "authn_login_success"; + internal const string AuthnLoginSuccessAfterFail = "authn_login_successafterfail"; + internal const string AuthnLoginFail = "authn_login_fail"; + internal const string AuthnLoginFailMax = "authn_login_fail_max"; + internal const string AuthnLoginLock = "authn_login_lock"; + internal const string AuthnPasswordChange = "authn_password_change"; + internal const string AuthnPasswordChangeFail = "authn_password_change_fail"; + internal const string AuthnImpossibleTravel = "authn_impossible_travel"; + internal const string AuthnTokenCreated = "authn_token_created"; + internal const string AuthnTokenRevoked = "authn_token_revoked"; + internal const string AuthnTokenReuse = "authn_token_reuse"; + internal const string AuthnTokenDelete = "authn_token_delete"; + + internal const string AuthzFail = "authz_fail"; + internal const string AuthzChange = "authz_change"; + internal const string AuthzAdmin = "authz_admin"; + + internal const string CryptDecryptFail = "crypt_decrypt_fail"; + internal const string CryptEncryptFail = "crypt_encrypt_fail"; + + internal const string ExcessRateLimitExceeded = "excess_rate_limit_exceeded"; + + internal const string UploadComplete = "upload_complete"; + internal const string UploadStored = "upload_stored"; + internal const string UploadValidation = "upload_validation"; + internal const string UploadDelete = "upload_delete"; + + internal const string InputValidationFailed = "input_validation_failed"; + internal const string InputValidationDiscreteFail = "input_validation_discrete_fail"; + + internal const string MaliciousExcess404 = "malicious_excess_404"; + internal const string MaliciousExtraneous = "malicious_extraneous"; + internal const string MaliciousAttackTool = "malicious_attack_tool"; + internal const string MaliciousCors = "malicious_cors"; + internal const string MaliciousDirectReference = "malicious_direct_reference"; + + internal const string PrivilegePermissionsChanged = "privilege_permissions_changed"; + + internal const string SensitiveCreate = "sensitive_create"; + internal const string SensitiveRead = "sensitive_read"; + internal const string SensitiveUpdate = "sensitive_update"; + internal const string SensitiveDelete = "sensitive_delete"; + + internal const string SequenceFail = "sequence_fail"; + + internal const string SessionCreated = "session_created"; + internal const string SessionRenewed = "session_renewed"; + internal const string SessionExpired = "session_expired"; + internal const string SessionUseAfterExpire = "session_use_after_expire"; + + internal const string SysStartup = "sys_startup"; + internal const string SysShutdown = "sys_shutdown"; + internal const string SysRestart = "sys_restart"; + internal const string SysCrash = "sys_crash"; + internal const string SysMonitorDisabled = "sys_monitor_disabled"; + internal const string SysMonitorEnabled = "sys_monitor_enabled"; + + internal const string UserCreated = "user_created"; + internal const string UserUpdated = "user_updated"; + internal const string UserArchived = "user_archived"; + internal const string UserDeleted = "user_deleted"; +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Authn.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Authn.cs new file mode 100644 index 0000000..a734834 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Authn.cs @@ -0,0 +1,465 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for authentication events. +/// +public partial class SecurityLogger +{ + /// + /// Record a successful login event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginSuccess( + string message, + string? userId, + params object?[] args) + { + LogAuthnLoginSuccess(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a successful login event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginSuccess( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnLoginSuccess, userId); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Information, message, metadata, args); + } + + /// + /// Record a successful login event after a previous failure. + /// + /// Log message. + /// User identifier. + /// Number of retries before success. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginSuccessAfterFail( + string message, + string? userId, + int? retries, + params object?[] args) + { + LogAuthnLoginSuccessAfterFail(message, userId, retries, new SecurityEventMetadata(), args); + } + + /// + /// Record a successful login event after a previous failure. + /// + /// Log message. + /// User identifier. + /// Number of retries before success. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginSuccessAfterFail( + string message, + string? userId, + int? retries, + SecurityEventMetadata? metadata, + params object?[] args) + { + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnLoginSuccessAfterFail, userId, retries?.ToString()); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Information, message, metadata, args); + } + + /// + /// Record a failed login event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginFail( + string message, + string? userId, + params object?[] args) + { + LogAuthnLoginFail(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a failed login event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginFail( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnLoginFail, userId); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record a failed login limit being reached event. + /// + /// Log message. + /// User identifier. + /// Max limit of retries. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginFailMax( + string message, + string? userId, + int? maxLimit, + params object?[] args) + { + LogAuthnLoginFailMax(message, userId, maxLimit, new SecurityEventMetadata(), args); + } + + /// + /// Record a failed login limit being reached event. + /// + /// Log message. + /// User identifier. + /// Max limit of retries. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginFailMax( + string message, + string? userId, + int? maxLimit, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId, maxLimit]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnLoginFailMax, userId, maxLimit?.ToString()); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record an account lockout event (e.g. due to multiple failed login attempts). + /// + /// Log message. + /// User identifier. + /// Lock reason. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginLock( + string message, + string? userId, + string? reason, + params object?[] args) + { + LogAuthnLoginLock(message, userId, reason, new SecurityEventMetadata(), args); + } + + /// + /// Record an account lockout event (e.g. due to multiple failed login attempts). + /// + /// Log message. + /// User identifier. + /// Lock reason. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnLoginLock( + string message, + string? userId, + string? reason, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId, reason]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnLoginLock, userId, reason); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record a successful password change event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogAuthnPasswordChange( + string message, + string? userId, + params object?[] args) + { + LogAuthnPasswordChange(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a successful password change event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnPasswordChange( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnPasswordChange, userId); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Information, message, metadata, args); + } + + /// + /// Record a failed password change event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogAuthnPasswordChangeFail( + string message, + string? userId, + params object?[] args) + { + LogAuthnPasswordChangeFail(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a failed password change event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnPasswordChangeFail( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnPasswordChangeFail, userId); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Critical, message, metadata, args); + } + + /// + /// Record a user logged in from one city suddenly appearing in another, too far away. + /// + /// + /// This often indicates and account takeover. + /// + /// Log message. + /// User identifier. + /// First region. + /// Second region. + /// An object array that contains zero or more objects to format. + public void LogAuthnImpossibleTravel( + string message, + string? userId, + string? regionOne, + string? regionTwo, + params object?[] args) + { + LogAuthnImpossibleTravel(message, userId, regionOne, regionTwo, new SecurityEventMetadata(), args); + } + + /// + /// Record a user logged in from one city suddenly appearing in another, too far away. + /// + /// + /// This often indicates and account takeover. + /// + /// Log message. + /// User identifier. + /// First region. + /// Second region. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnImpossibleTravel( + string message, + string? userId, + string? regionOne, + string? regionTwo, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId, regionOne, regionTwo]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnImpossibleTravel, userId, regionOne, regionTwo); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Critical, message, metadata, args); + } + + /// + /// Record a token creation event. + /// + /// Log message. + /// User/service identifier. + /// Entitlements (e.g. create, read, update, etc.). + /// An object array that contains zero or more objects to format. + public void LogAuthnTokenCreated( + string message, + string? userId, + IEnumerable? entitlements, + params object?[] args) + { + LogAuthnTokenCreated(message, userId, entitlements, new SecurityEventMetadata(), args); + } + + /// + /// Record a token creation event. + /// + /// Log message. + /// User/service identifier. + /// Entitlements (e.g. create, read, update, etc.). + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnTokenCreated( + string message, + string? userId, + IEnumerable? entitlements, + SecurityEventMetadata metadata, + params object?[] args) + { + var commaSeparatedEntitlements = entitlements is not null + ? string.Join(",", entitlements) + : null; + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnTokenCreated, userId, commaSeparatedEntitlements); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Information, message, metadata, args); + } + + /// + /// Record a token revocation event. + /// + /// Log message. + /// User/service identifier. + /// Token identifier. + /// An object array that contains zero or more objects to format. + public void LogAuthnTokenRevoked( + string message, + string? userId, + string? tokenId, + params object?[] args) + { + LogAuthnTokenRevoked(message, userId, tokenId, new SecurityEventMetadata(), args); + } + + /// + /// Record a token revocation event. + /// + /// Log message. + /// User/service identifier. + /// Token identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnTokenRevoked( + string message, + string? userId, + string? tokenId, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId, tokenId]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnTokenRevoked, userId, tokenId); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Information, message, metadata, args); + } + + /// + /// Record an attempted token reuse event after a token has been revoked. + /// + /// Log message. + /// User/service identifier. + /// Token identifier. + /// An object array that contains zero or more objects to format. + public void LogAuthnTokenReuse( + string message, + string? userId, + string? tokenId, + params object?[] args) + { + LogAuthnTokenReuse(message, userId, tokenId, new SecurityEventMetadata(), args); + } + + /// + /// Record an attempted token reuse event after a token has been revoked. + /// + /// Log message. + /// User/service identifier. + /// Token identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnTokenReuse( + string message, + string? userId, + string? tokenId, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId, tokenId]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnTokenReuse, userId, tokenId); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Critical, message, metadata, args); + } + + /// + /// Record a token deletion event. + /// + /// Log message. + /// User/service identifier. + /// An object array that contains zero or more objects to format. + public void LogAuthnTokenDelete( + string message, + string? userId, + params object?[] args) + { + LogAuthnTokenDelete(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a token deletion event. + /// + /// Log message. + /// User/service identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthnTokenDelete( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + args = args.Concat([userId]).ToArray(); + + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthnTokenDelete, userId); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Authz.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Authz.cs new file mode 100644 index 0000000..1982a69 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Authz.cs @@ -0,0 +1,124 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for authorization events. +/// +public partial class SecurityLogger +{ + /// + /// Record and authorization failure event. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// An object array that contains zero or more objects to format. + public void LogAuthzFail( + string message, + string? userId, + string? resource, + params object?[] args) + { + LogAuthzFail(message, userId, resource, new SecurityEventMetadata(), args); + } + + /// + /// Record and authorization failure event. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthzFail( + string message, + string? userId, + string? resource, + SecurityEventMetadata metadata, + params object?[] args) + { + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthzFail, userId, resource); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Critical, message, metadata, args); + } + + /// + /// Record an authorization change event. + /// + /// Log message. + /// User identifier. + /// Original authorization. + /// New authorization. + /// An object array that contains zero or more objects to format. + public void LogAuthzChange( + string message, + string? userId, + string? from, + string? to, + params object?[] args) + { + LogAuthzChange(message, userId, from, to, new SecurityEventMetadata(), args); + } + + /// + /// Record an authorization change event. + /// + /// Log message. + /// User identifier. + /// Original authorization. + /// New authorization. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthzChange( + string message, + string? userId, + string? from, + string? to, + SecurityEventMetadata metadata, + params object?[] args) + { + var @event = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthzChange, userId, from, to); + metadata ??= new SecurityEventMetadata(); + + Log(@event, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record an authorized administration event (e.g. user privilege change). + /// + /// Log message. + /// User identifier. + /// Event description. + /// An object array that contains zero or more objects to format. + public void LogAuthzAdmin( + string message, + string? userId, + string? @event, + params object?[] args) + { + LogAuthzAdmin(message, userId, @event, new SecurityEventMetadata(), args); + } + + /// + /// Record an authorized administration event. + /// + /// Log message. + /// User identifier. + /// Event description. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogAuthzAdmin( + string message, + string? userId, + string? @event, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.AuthzAdmin, userId, @event); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Crypt.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Crypt.cs new file mode 100644 index 0000000..4e4957d --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Crypt.cs @@ -0,0 +1,75 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for cryptography events. +/// +public partial class SecurityLogger +{ + /// + /// Record a decryption failure event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogCryptDecryptFail( + string message, + string? userId, + params object?[] args) + { + LogCryptDecryptFail(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a decryption failure event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogCryptDecryptFail( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.CryptDecryptFail, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record an encryption failure event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogCryptEncryptFail( + string message, + string? userId, + params object?[] args) + { + LogCryptEncryptFail(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record an encryption failure event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogCryptEncryptFail( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.CryptEncryptFail, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Excess.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Excess.cs new file mode 100644 index 0000000..05d5487 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Excess.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for excess events. +/// +public partial class SecurityLogger +{ + /// + /// Record a rate limit or service limit being exceeded event. + /// + /// Log message. + /// User identifier. + /// Maximum allowed requests. + /// An object array that contains zero or more objects to format. + public void LogExcessRateLimitExceeded( + string message, + string? userId, + int? max, + params object?[] args) + { + LogExcessRateLimitExceeded(message, userId, max, new SecurityEventMetadata(), args); + } + + /// + /// Record a rate limit or service limit being exceeded event. + /// + /// Log message. + /// User identifier. + /// Maximum allowed requests. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogExcessRateLimitExceeded( + string message, + string? userId, + int? max, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.ExcessRateLimitExceeded, userId, max?.ToString()); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Input.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Input.cs new file mode 100644 index 0000000..235dc20 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Input.cs @@ -0,0 +1,87 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for input events. +/// +public partial class SecurityLogger +{ + /// + /// Record an input validation failed event. + /// + /// Log message. + /// Invalid fields. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogInputValidationFailed( + string message, + IEnumerable? fields, + string? userId, + params object?[] args) + { + LogInputValidationFailed(message, fields, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record an input validation failed event. + /// + /// Log message. + /// Invalid fields. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogInputValidationFailed( + string message, + IEnumerable? fields, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var encapsulatedFields = fields is not null + ? $"({string.Join(",", fields)})" + : null; + + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.InputValidationFailed, encapsulatedFields, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record a discrete input validation fail. + /// + /// Log message. + /// Invalid field. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogInputValidationDiscreteFail( + string message, + string? field, + string? userId, + params object?[] args) + { + LogInputValidationDiscreteFail(message, field, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a discrete input validation fail. + /// + /// Log message. + /// Invalid field. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogInputValidationDiscreteFail( + string message, + string? field, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.InputValidationDiscreteFail, field, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Malicious.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Malicious.cs new file mode 100644 index 0000000..bb0d053 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Malicious.cs @@ -0,0 +1,212 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for malicious events. +/// +public partial class SecurityLogger +{ + /// + /// Record excessive 404 errors. + /// + /// + /// Could indicate a reconnaissance attempt or automated attack. + /// + /// Log message. + /// IP address. + /// User agent. + /// An object array that contains zero or more objects to format. + public void LogMaliciousExcess404( + string message, + string? ipAddress, + string? useragent, + params object?[] args) + { + LogMaliciousExcess404(message, ipAddress, useragent, new SecurityEventMetadata(), args); + } + + /// + /// Record excessive 404 errors. + /// + /// + /// Could indicate a reconnaissance attempt or automated attack. + /// + /// Log message. + /// IP address. + /// User agent. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogMaliciousExcess404( + string message, + string? ipAddress, + string? useragent, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.MaliciousExcess404, ipAddress, useragent); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record unexpected input or extraneous data. + /// + /// Log message. + /// IP address. + /// Input name. + /// User agent. + /// An object array that contains zero or more objects to format. + public void LogMaliciousExtraneous( + string message, + string? ipAddress, + string? inputName, + string? useragent, + params object?[] args) + { + LogMaliciousExtraneous(message, ipAddress, inputName, useragent, new SecurityEventMetadata(), args); + } + + /// + /// Record unexpected input or extraneous data. + /// + /// Log message. + /// IP address. + /// Input name. + /// User agent. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogMaliciousExtraneous( + string message, + string? ipAddress, + string? inputName, + string? useragent, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.MaliciousExtraneous, ipAddress, inputName, useragent); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Critical, message, metadata, args); + } + + /// + /// Record detection of attack tool usage. + /// + /// Log message. + /// IP address. + /// Tool name. + /// User agent. + /// An object array that contains zero or more objects to format. + public void LogMaliciousAttackTool( + string message, + string? ipAddress, + string? toolName, + string? useragent, + params object?[] args) + { + LogMaliciousAttackTool(message, ipAddress, toolName, useragent, new SecurityEventMetadata(), args); + } + + /// + /// Record detection of attack tool usage. + /// + /// Log message. + /// IP address. + /// Tool name. + /// User agent. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogMaliciousAttackTool( + string message, + string? ipAddress, + string? toolName, + string? useragent, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.MaliciousAttackTool, ipAddress, toolName, useragent); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Critical, message, metadata, args); + } + + /// + /// Record a CORS violation attempt. + /// + /// Log message. + /// IP address. + /// Referrer. + /// User agent. + /// An object array that contains zero or more objects to format. + public void LogMaliciousCors( + string message, + string? ipAddress, + string? useragent, + string? referrer, + params object?[] args) + { + LogMaliciousCors(message, ipAddress, useragent, referrer, new SecurityEventMetadata(), args); + } + + /// + /// Record a CORS violation attempt. + /// + /// Log message. + /// IP address. + /// User agent. + /// Referrer. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogMaliciousCors( + string message, + string? ipAddress, + string? useragent, + string? referrer, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.MaliciousCors, ipAddress, useragent, referrer); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Critical, message, metadata, args); + } + + /// + /// Record a direct object reference attempt. + /// + /// Log message. + /// IP address. + /// User agent. + /// An object array that contains zero or more objects to format. + public void LogMaliciousDirectReference( + string message, + string? ipAddress, + string? useragent, + params object?[] args) + { + LogMaliciousDirectReference(message, ipAddress, useragent, new SecurityEventMetadata(), args); + } + + /// + /// Record a direct object reference attempt. + /// + /// Log message. + /// IP address. + /// User agent. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogMaliciousDirectReference( + string message, + string? ipAddress, + string? useragent, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.MaliciousDirectReference, ipAddress, useragent); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Critical, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Privilege.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Privilege.cs new file mode 100644 index 0000000..bf6d378 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Privilege.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for privilege events. +/// +public partial class SecurityLogger +{ + /// + /// Record a permission level change. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// Original privilege level. + /// New privilege level. + /// An object array that contains zero or more objects to format. + public void LogPrivilegePermissionsChanged( + string message, + string? userId, + string? resource, + string? fromLevel, + string? toLevel, + params object?[] args) + { + LogPrivilegePermissionsChanged(message, userId, resource, fromLevel, toLevel, new SecurityEventMetadata(), args); + } + + /// + /// Record a permission level change. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// Original privilege level. + /// New privilege level. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogPrivilegePermissionsChanged( + string message, + string? userId, + string? resource, + string? fromLevel, + string? toLevel, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.PrivilegePermissionsChanged, userId, resource, fromLevel, toLevel); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Sensitive.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Sensitive.cs new file mode 100644 index 0000000..af97c61 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Sensitive.cs @@ -0,0 +1,157 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for sensitive data events. +/// +public partial class SecurityLogger +{ + /// + /// record creation of a sensitive object. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// An object array that contains zero or more objects to format. + public void LogSensitiveCreate( + string message, + string? userId, + string? resource, + params object?[] args) + { + LogSensitiveCreate(message, userId, resource, new SecurityEventMetadata(), args); + } + + /// + /// Record creation of a sensitive object. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSensitiveCreate( + string message, + string? userId, + string? resource, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SensitiveCreate, userId, resource); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record access (read) of sensitive data. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// An object array that contains zero or more objects to format. + public void LogSensitiveRead( + string message, + string? userId, + string? resource, + params object?[] args) + { + LogSensitiveRead(message, userId, resource, new SecurityEventMetadata(), args); + } + + /// + /// Record access (read) of sensitive data. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSensitiveRead( + string message, + string? userId, + string? resource, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SensitiveRead, userId, resource); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record modification of sensitive data. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// An object array that contains zero or more objects to format. + public void LogSensitiveUpdate( + string message, + string? userId, + string? resource, + params object?[] args) + { + LogSensitiveUpdate(message, userId, resource, new SecurityEventMetadata(), args); + } + + /// + /// Record modification of sensitive data. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSensitiveUpdate( + string message, + string? userId, + string? resource, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SensitiveUpdate, userId, resource); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record deletion of sensitive data. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// An object array that contains zero or more objects to format. + public void LogSensitiveDelete( + string message, + string? userId, + string? resource, + params object?[] args) + { + LogSensitiveDelete(message, userId, resource, new SecurityEventMetadata(), args); + } + + /// + /// Record deletion of sensitive data. + /// + /// Log message. + /// User identifier. + /// Resource identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSensitiveDelete( + string message, + string? userId, + string? resource, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SensitiveDelete, userId, resource); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Sequence.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Sequence.cs new file mode 100644 index 0000000..31ba2b0 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Sequence.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for sequence events. +/// +public partial class SecurityLogger +{ + /// + /// Record a sequence error (unexpected order of action). + /// + /// + /// Could indicate intentional abuse of the business logic. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogSequenceFail( + string message, + string? userId, + params object?[] args) + { + LogSequenceFail(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a sequence error (unexpected order of action). + /// + /// + /// Could indicate intentional abuse of the business logic. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSequenceFail( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SequenceFail, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Critical, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Session.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Session.cs new file mode 100644 index 0000000..47f4091 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Session.cs @@ -0,0 +1,145 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for session events. +/// +public partial class SecurityLogger +{ + /// + /// Record session creation. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogSessionCreated( + string message, + string? userId, + params object?[] args) + { + LogSessionCreated(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record session creation. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSessionCreated( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SessionCreated, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Information, message, metadata, args); + } + + /// + /// Record session renewal. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogSessionRenewed( + string message, + string? userId, + params object?[] args) + { + LogSessionRenewed(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record session renewal. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSessionRenewed( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SessionRenewed, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Information, message, metadata, args); + } + + /// + /// Record session expiration. + /// + /// Log message. + /// User identifier. + /// Expiration reason. + /// An object array that contains zero or more objects to format. + public void LogSessionExpired( + string message, + string? userId, + string? reason, + params object?[] args) + { + LogSessionExpired(message, userId, reason, new SecurityEventMetadata(), args); + } + + /// + /// Record session expiration. + /// + /// Log message. + /// User identifier. + /// Expiration reason. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSessionExpired( + string message, + string? userId, + string? reason, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SessionExpired, userId, reason); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Information, message, metadata, args); + } + + /// + /// Record attempt to use an expired session. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogSessionUseAfterExpire( + string message, + string? userId, + params object?[] args) + { + LogSessionUseAfterExpire(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record attempt to use an expired session. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSessionUseAfterExpire( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SessionUseAfterExpire, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Critical, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.System.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.System.cs new file mode 100644 index 0000000..7b3df5c --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.System.cs @@ -0,0 +1,215 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for system events. +/// +public partial class SecurityLogger +{ + /// + /// Record a system startup event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogSysStartup( + string message, + string? userId, + params object?[] args) + { + LogSysStartup(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a system startup event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSysStartup( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SysStartup, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record a system shutdown event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogSysShutdown( + string message, + string? userId, + params object?[] args) + { + LogSysShutdown(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a system shutdown event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSysShutdown( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SysShutdown, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record a system restart event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogSysRestart( + string message, + string? userId, + params object?[] args) + { + LogSysRestart(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a system restart event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSysRestart( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SysRestart, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record a system crash event. + /// + /// Log message. + /// User identifier. + /// An object array that contains zero or more objects to format. + public void LogSysCrash( + string message, + string? userId, + params object?[] args) + { + LogSysCrash(message, userId, new SecurityEventMetadata(), args); + } + + /// + /// Record a system crash event. + /// + /// Log message. + /// User identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSysCrash( + string message, + string? userId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SysCrash, userId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record disabling of a system monitor. + /// + /// Log message. + /// User identifier. + /// Monitor name. + /// An object array that contains zero or more objects to format. + public void LogSysMonitorDisabled( + string message, + string? userId, + string? monitor, + params object?[] args) + { + LogSysMonitorDisabled(message, userId, monitor, new SecurityEventMetadata(), args); + } + + /// + /// Record disabling of a system monitor. + /// + /// Log message. + /// User identifier. + /// Monitor name. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSysMonitorDisabled( + string message, + string? userId, + string? monitor, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SysMonitorDisabled, userId, monitor); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record enabling of a system monitor. + /// + /// Log message. + /// User identifier. + /// Monitor name. + /// An object array that contains zero or more objects to format. + public void LogSysMonitorEnabled( + string message, + string? userId, + string? monitor, + params object?[] args) + { + LogSysMonitorEnabled(message, userId, monitor, new SecurityEventMetadata(), args); + } + + /// + /// Record enabling of a system monitor. + /// + /// Log message. + /// User identifier. + /// Monitor name. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogSysMonitorEnabled( + string message, + string? userId, + string? monitor, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.SysMonitorEnabled, userId, monitor); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Upload.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Upload.cs new file mode 100644 index 0000000..dd66379 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.Upload.cs @@ -0,0 +1,177 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for upload events. +/// +public partial class SecurityLogger +{ + /// + /// Record a successful upload event. + /// + /// Log message. + /// User identifier. + /// File name. + /// File type. + /// An object array that contains zero or more objects to format. + public void LogUploadComplete( + string message, + string? userId, + string? fileName, + string? fileType, + params object?[] args) + { + LogUploadComplete(message, userId, fileName, fileType, new SecurityEventMetadata(), args); + } + + /// + /// Record a successful upload event. + /// + /// Log message. + /// User identifier. + /// File name. + /// File type. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogUploadComplete( + string message, + string? userId, + string? fileName, + string? fileType, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.UploadComplete, userId, fileName, fileType); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Information, message, metadata, args); + } + + /// + /// Record a successful file storage event. + /// + /// Log message. + /// User identifier. + /// Original storage location. + /// New storage location. + /// An object array that contains zero or more objects to format. + public void LogUploadStored( + string message, + string? userId, + string? from, + string? to, + params object?[] args) + { + LogUploadStored(message, userId, from, to, new SecurityEventMetadata(), args); + } + + /// + /// Record a successful file storage event. + /// + /// Log message. + /// User identifier. + /// Original storage location. + /// New storage location. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogUploadStored( + string message, + string? userId, + string? from, + string? to, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.UploadStored, userId, from, to); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Information, message, metadata, args); + } + + /// + /// Record the results of a file validation process. + /// + /// Log message. + /// User identifier. + /// File name. + /// Validation type (e.g. virusscan, signature, size, etc.). + /// Validation result (e.g. FAILED, incomplete, passed). + /// Log level. + /// An object array that contains zero or more objects to format. + public void LogUploadValidation( + string message, + string? userId, + string? filename, + string? validationType, + string? result, + LogLevel level, + params object?[] args) + { + LogUploadValidation(message, userId, filename, validationType, result, level, new SecurityEventMetadata(), args); + } + + /// + /// Record the results of a file validation process. + /// + /// Log message. + /// User identifier. + /// File name. + /// Validation type (e.g. virusscan, signature, size, etc.). + /// Validation result (e.g. FAILED, incomplete, passed). + /// Log level. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogUploadValidation( + string message, + string? userId, + string? filename, + string? validationType, + string? result, + LogLevel level, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.UploadValidation, userId, filename, validationType, result); + metadata ??= new SecurityEventMetadata(); + + Log(evt, level, message, metadata, args); + } + + /// + /// Record a file deletion event. + /// + /// Log message. + /// User identifier. + /// File identifier. + /// An object array that contains zero or more objects to format. + public void LogUploadDelete( + string message, + string? userId, + string? fileId, + params object?[] args) + { + LogUploadDelete(message, userId, fileId, new SecurityEventMetadata(), args); + } + + /// + /// Record a file deletion event. + /// + /// Log message. + /// User identifier. + /// File identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogUploadDelete( + string message, + string? userId, + string? fileId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.UploadDelete, userId, fileId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Information, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.User.cs b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.User.cs new file mode 100644 index 0000000..be276cb --- /dev/null +++ b/src/ByteGuard.SecurityLogger/Partials/SecurityLogger.User.cs @@ -0,0 +1,173 @@ +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security logger functionality for user events. +/// +public partial class SecurityLogger +{ + /// + /// Record creation of a new user. + /// + /// Log message. + /// User identifier. + /// New user identifier. + /// New user attributes. + /// An object array that contains zero or more objects to format. + public void LogUserCreated( + string message, + string? userId, + string? newUserId, + Dictionary>? attributes, + params object?[] args) + { + LogUserCreated(message, userId, newUserId, attributes, new SecurityEventMetadata(), args); + } + + /// + /// Record creation of a new user. + /// + /// Log message. + /// User identifier. + /// New user identifier. + /// New user attributes. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogUserCreated( + string message, + string? userId, + string? newUserId, + Dictionary>? attributes, + SecurityEventMetadata metadata, + params object?[] args) + { + var commaSeparatedAttributes = attributes is not null + ? string.Join(",", attributes?.Select(kvp => $"{kvp.Key}:{string.Join(",", kvp.Value)}")) + : null; + + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.UserCreated, userId, newUserId, commaSeparatedAttributes); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record update of a user's attributes. + /// + /// Log message. + /// User identifier. + /// On user identifier. + /// User attributes. + /// An object array that contains zero or more objects to format. + public void LogUserUpdated( + string message, + string? userId, + string? onUserId, + Dictionary>? attributes, + params object?[] args) + { + LogUserUpdated(message, userId, onUserId, attributes, new SecurityEventMetadata(), args); + } + + /// + /// Record update of a user's attributes. + /// + /// Log message. + /// User identifier. + /// On user identifier. + /// User attributes. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogUserUpdated( + string message, + string? userId, + string? onUserId, + Dictionary>? attributes, + SecurityEventMetadata metadata, + params object?[] args) + { + var commaSeparatedAttributes = attributes is not null + ? string.Join(",", attributes.Select(kvp => $"{kvp.Key}:{string.Join(",", kvp.Value)}")) + : null; + + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.UserUpdated, userId, onUserId, commaSeparatedAttributes); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record archiving of a user. + /// + /// Log message. + /// User identifier. + /// On user identifier. + /// An object array that contains zero or more objects to format. + public void LogUserArchived( + string message, + string? userId, + string? onUserId, + params object?[] args) + { + LogUserArchived(message, userId, onUserId, new SecurityEventMetadata(), args); + } + + /// + /// Record archiving of a user. + /// + /// Log message. + /// User identifier. + /// On user identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogUserArchived( + string message, + string? userId, + string? onUserId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.UserArchived, userId, onUserId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } + + /// + /// Record deletion of a user. + /// + /// Log message. + /// User identifier. + /// On user identifier. + /// An object array that contains zero or more objects to format. + public void LogUserDeleted( + string message, + string? userId, + string? onUserId, + params object?[] args) + { + LogUserDeleted(message, userId, onUserId, new SecurityEventMetadata(), args); + } + + /// + /// Record deletion of a user. + /// + /// Log message. + /// User identifier. + /// On user identifier. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void LogUserDeleted( + string message, + string? userId, + string? onUserId, + SecurityEventMetadata metadata, + params object?[] args) + { + var evt = EventLabelBuilder.BuildEventString(LoggingVocabulary.UserDeleted, userId, onUserId); + metadata ??= new SecurityEventMetadata(); + + Log(evt, LogLevel.Warning, message, metadata, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/SecurityEventMetadata.cs b/src/ByteGuard.SecurityLogger/SecurityEventMetadata.cs new file mode 100644 index 0000000..4053ac0 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/SecurityEventMetadata.cs @@ -0,0 +1,57 @@ +namespace ByteGuard.SecurityLogger; + +/// +/// Metadata associated with a security event. +/// +public record SecurityEventMetadata +{ + /// + /// User agent of the caller. + /// + public string? UserAgent { get; set; } + + /// + /// IP-address of the caller. + /// + public string? SourceIp { get; set; } + + /// + /// IP-address of the host. + /// + public string? HostIp { get; set; } + + /// + /// Hostname of the host. + /// + public string? Hostname { get; set; } + + /// + /// Protocol used for the request. + /// + public string? Protocol { get; set; } + + /// + /// Port used for the request. + /// + public string? Port { get; set; } + + /// + /// Relative request URI. + /// + public string? RequestUri { get; set; } + + /// + /// Request method. + /// + public string? RequestMethod { get; set; } + + /// + /// Region of the host. + /// + public string? Region { get; set; } + + /// + /// Geographical location of the host. + /// + public string? Geo { get; set; } +} diff --git a/src/ByteGuard.SecurityLogger/SecurityLogger.cs b/src/ByteGuard.SecurityLogger/SecurityLogger.cs new file mode 100644 index 0000000..7ee6859 --- /dev/null +++ b/src/ByteGuard.SecurityLogger/SecurityLogger.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Logging; +using ByteGuard.SecurityLogger.Enrichers; +using ByteGuard.SecurityLogger.Configuration; + +namespace ByteGuard.SecurityLogger; + +/// +/// Security specific ILogging extensions. +/// +public partial class SecurityLogger +{ + private readonly ILogger _logger; + private readonly SecurityLoggerConfiguration _configuration; + + /// + /// Instantiate a new security logger. + /// + /// ILogger implementation. + /// Security logger configuration. + public SecurityLogger(ILogger logger, SecurityLoggerConfiguration configuration) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + ConfigurationValidator.ThrowIfInvalid(configuration); + } + + /// + /// Generic log method. + /// + /// Security event. + /// Log level. + /// Log message. + /// Security event metadata. + /// An object array that contains zero or more objects to format. + public void Log(string @event, LogLevel level, string message, SecurityEventMetadata metadata, params object?[] args) + { + var properties = new Dictionary + { + ["AppId"] = _configuration.AppId, + ["Event"] = @event + }; + + PropertiesEnricher.PopulatePropertiesFromMetadata(properties, metadata, _configuration); + + using var _ = _logger.BeginScope(properties); + + _logger.Log(level, message, args); + } +} diff --git a/src/ByteGuard.SecurityLogger/SecurityLoggerFactoryExtensions.cs b/src/ByteGuard.SecurityLogger/SecurityLoggerFactoryExtensions.cs new file mode 100644 index 0000000..fbdf51c --- /dev/null +++ b/src/ByteGuard.SecurityLogger/SecurityLoggerFactoryExtensions.cs @@ -0,0 +1,18 @@ +using ByteGuard.SecurityLogger.Configuration; +using Microsoft.Extensions.Logging; + +namespace ByteGuard.SecurityLogger; + +/// +/// Extension methods for creating a security logger based on an existing ILogger implementation. +/// +public static class SecurityLoggerFactoryExtensions +{ + /// + /// Instantiate a from an existing ILogger implementation. + /// + /// ILogger implementation. + /// Security logger configuration. + public static SecurityLogger AsSecurityLogger(this ILogger logger, SecurityLoggerConfiguration configuration) => + new SecurityLogger(logger, configuration); +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Builders/EventLabelBuilderTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Builders/EventLabelBuilderTests.cs new file mode 100644 index 0000000..414c6a3 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Builders/EventLabelBuilderTests.cs @@ -0,0 +1,31 @@ +namespace ByteGuard.SecurityLogger.Tests.Unit.Builders; + +public class EventLabelBuilderTests +{ + [Fact(DisplayName = "BuildEventString should return event name when no arguments are provided")] + public void BuildEventString_NoArguments_ReturnsEventName() + { + // Arrange + var eventName = "test_event"; + + // Act + var result = EventLabelBuilder.BuildEventString(eventName); + + // Assert + Assert.Equal(eventName, result); + } + + [Theory(DisplayName = "BuildEventString should return event name with arguments when arguments are provided")] + [InlineData("test_event", new string?[] { "Arg1", "Arg2", null }, "test_event:Arg1,Arg2")] + [InlineData("test_event", new string?[] { null, "Arg2", null }, "test_event:Arg2")] + [InlineData("test_event", new string?[] { null }, "test_event")] + [InlineData("test_event", null, "test_event")] + public void BuildEventString_WithArguments_ReturnsEventNameWithArguments(string eventName, string?[] eventArgs, string expected) + { + // Act + var result = EventLabelBuilder.BuildEventString(eventName, eventArgs); + + // Assert + Assert.Equal(expected, result); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/ByteGuard.SecurityLogger.Tests.Unit.csproj b/tests/ByteGuard.SecurityLogger.Tests.Unit/ByteGuard.SecurityLogger.Tests.Unit.csproj index 9603054..b7bb6c8 100644 --- a/tests/ByteGuard.SecurityLogger.Tests.Unit/ByteGuard.SecurityLogger.Tests.Unit.csproj +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/ByteGuard.SecurityLogger.Tests.Unit.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable false @@ -12,6 +12,7 @@ + @@ -25,4 +26,10 @@ + + + + + + diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/ConfigurationValidatorTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/ConfigurationValidatorTests.cs new file mode 100644 index 0000000..ee616c1 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/ConfigurationValidatorTests.cs @@ -0,0 +1,23 @@ +using ByteGuard.SecurityLogger.Configuration; + +namespace ByteGuard.SecurityLogger.Tests.Unit; + +public class ConfigurationValidatorTests +{ + [Fact(DisplayName = "ThrowIfInvalid throws ArgumentNullException when configuration is null")] + public void ThrowIfInvalid_ThrowsArgumentNullException_WhenConfigurationIsNull() + { + Assert.Throws(() => ConfigurationValidator.ThrowIfInvalid(null!)); + } + + [Theory(DisplayName = "ThrowIfInvalid throws ArgumentException when AppId is null or empty")] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ThrowIfInvalid_ThrowsArgumentException_WhenAppIdIsNullOrEmpty(string appId) + { + var configuration = new SecurityLoggerConfiguration { AppId = appId }; + + Assert.Throws(() => ConfigurationValidator.ThrowIfInvalid(configuration)); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Helpers/FakeLoggerExtensions.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Helpers/FakeLoggerExtensions.cs new file mode 100644 index 0000000..f7ffb09 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Helpers/FakeLoggerExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Helpers; + +public static class FakeLoggerExtensions +{ + public static IReadOnlyDictionary GetScopeDictionary(this FakeLogRecord record) + { + var scope = record.Scopes.FirstOrDefault(scope => + { + var dict = scope as IReadOnlyDictionary; + return dict != null && dict.ContainsKey("Event"); + }); + + return (IReadOnlyDictionary)scope!; + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/AuthnTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/AuthnTests.cs new file mode 100644 index 0000000..f50953e --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/AuthnTests.cs @@ -0,0 +1,652 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class AuthnTests +{ + [Fact(DisplayName = "LogAuthnLoginSuccess without metadata should log message with correct values")] + public void LogAuthnLoginSuccess_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_login_success:{userId}"; + var expectedMessage = $"User {userId} successfully logged in."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginSuccess("User {UserId} successfully logged in.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginSuccess with metadata should log message with correct values")] + public void LogAuthnLoginSuccess_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_login_success:{userId}"; + var expectedMessage = $"User {userId} successfully logged in."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginSuccess("User {UserId} successfully logged in.", userId, userId, new()); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginSuccessAfterFail without metadata should log message with correct values")] + public void LogAuthnLoginSuccessAfterFail_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var retries = 3; + + var expectedEvent = $"authn_login_successafterfail:{userId},{retries}"; + var expectedMessage = $"User {userId} successfully logged in after {retries} attempt(s)."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginSuccessAfterFail("User {UserId} successfully logged in after {Retries} attempt(s).", userId, retries, userId, retries); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginSuccessAfterFail with metadata should log message with correct values")] + public void LogAuthnLoginSuccessAfterFail_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var retries = 3; + + var expectedEvent = $"authn_login_successafterfail:{userId},{retries}"; + var expectedMessage = $"User {userId} successfully logged in after {retries} attempt(s)."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginSuccessAfterFail("User {UserId} successfully logged in after {Retries} attempt(s).", userId, retries, userId, retries, new()); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginFail without metadata should log message with correct values")] + public void LogAuthnLoginFail_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_login_fail:{userId}"; + var expectedMessage = $"User {userId} failed to log in."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginFail("User {UserId} failed to log in.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginFail with metadata should log message with correct values")] + public void LogAuthnLoginFail_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_login_fail:{userId}"; + var expectedMessage = $"User {userId} failed to log in."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginFail("User {UserId} failed to log in.", userId, userId, new()); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginFailMax without metadata should log message with correct values")] + public void LogAuthnLoginFailMax_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var maxLimit = 5; + + var expectedEvent = $"authn_login_fail_max:{userId},{maxLimit}"; + var expectedMessage = $"User {userId} failed to log in after {maxLimit} attempts."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginFailMax("User {UserId} failed to log in after {MaxRetries} attempts.", userId, maxLimit, userId, maxLimit); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginFailMax with metadata should log message with correct values")] + public void LogAuthnLoginFailMax_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var maxLimit = 5; + + var expectedEvent = $"authn_login_fail_max:{userId},{maxLimit}"; + var expectedMessage = $"User {userId} failed to log in after {maxLimit} attempts."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginFailMax("User {UserId} failed to log in after {MaxRetries} attempts.", userId, maxLimit, userId, maxLimit, new()); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginLock without metadata should log message with correct values")] + public void LogAuthnLoginLock_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var reason = "maxretries"; + + var expectedEvent = $"authn_login_lock:{userId},{reason}"; + var expectedMessage = $"User {userId} was locked out with reason {reason}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginLock("User {UserId} was locked out with reason {Reason}.", userId, reason, userId, reason); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnLoginLock with metadata should log message with correct values")] + public void LogAuthnLoginLock_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var reason = "maxretries"; + + var expectedEvent = $"authn_login_lock:{userId},{reason}"; + var expectedMessage = $"User {userId} was locked out with reason {reason}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnLoginLock("User {UserId} was locked out with reason {Reason}.", userId, reason, new(), userId, reason); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnPasswordChange without metadata should log message with correct values")] + public void LogAuthnPasswordChange_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_password_change:{userId}"; + var expectedMessage = $"User {userId} successfully changed their password."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnPasswordChange("User {UserId} successfully changed their password.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnPasswordChange with metadata should log message with correct values")] + public void LogAuthnPasswordChange_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_password_change:{userId}"; + var expectedMessage = $"User {userId} successfully changed their password."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnPasswordChange("User {UserId} successfully changed their password.", userId, userId, new()); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnPasswordChangeFail without metadata should log message with correct values")] + public void LogAuthnPasswordChangeFail_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_password_change_fail:{userId}"; + var expectedMessage = $"User {userId} failed to change their password."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnPasswordChangeFail("User {UserId} failed to change their password.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnPasswordChangeFail with metadata should log message with correct values")] + public void LogAuthnPasswordChangeFail_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_password_change_fail:{userId}"; + var expectedMessage = $"User {userId} failed to change their password."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnPasswordChangeFail("User {UserId} failed to change their password.", userId, userId, new()); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnImpossibleTravel without metadata should log message with correct values")] + public void LogAuthnImpossibleTravel_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var regionOne = "US-OR"; + var regionTwo = "CN-SH"; + + var expectedEvent = $"authn_impossible_travel:{userId},{regionOne},{regionTwo}"; + var expectedMessage = $"User {userId} attempted impossible travel."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnImpossibleTravel("User {UserId} attempted impossible travel.", userId, regionOne, regionTwo, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnImpossibleTravel with metadata should log message with correct values")] + public void LogAuthnImpossibleTravel_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var regionOne = "US-OR"; + var regionTwo = "CN-SH"; + + var expectedEvent = $"authn_impossible_travel:{userId},{regionOne},{regionTwo}"; + var expectedMessage = $"User {userId} attempted impossible travel."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnImpossibleTravel("User {UserId} attempted impossible travel.", userId, regionOne, regionTwo, new(), userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnTokenCreated without metadata should log message with correct values")] + + public void LogAuthnTokenCreated_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var entitlements = new[] { "read", "write" }; + var commaSeparatedEntitlements = "read,write"; + + var expectedEvent = $"authn_token_created:{userId},{commaSeparatedEntitlements}"; + var expectedMessage = $"User {userId} created a new token."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnTokenCreated("User {UserId} created a new token.", userId, entitlements, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnTokenCreated with metadata should log message with correct values")] + public void LogAuthnTokenCreated_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var entitlements = new[] { "read", "write" }; + var commaSeparatedEntitlements = "read,write"; + + var expectedEvent = $"authn_token_created:{userId},{commaSeparatedEntitlements}"; + var expectedMessage = $"User {userId} created a new token."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnTokenCreated("User {UserId} created a new token.", userId, entitlements, new(), userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnTokenRevoked without metadata should log message with correct values")] + public void LogAuthnTokenRevoked_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var tokenId = "xyc-abc-123-gfk"; + + var expectedEvent = $"authn_token_revoked:{userId},{tokenId}"; + var expectedMessage = $"User {userId} revoked a token."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnTokenRevoked("User {UserId} revoked a token.", userId, tokenId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnTokenRevoked with metadata should log message with correct values")] + public void LogAuthnTokenRevoked_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var tokenId = "xyc-abc-123-gfk"; + + var expectedEvent = $"authn_token_revoked:{userId},{tokenId}"; + var expectedMessage = $"User {userId} revoked a token."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnTokenRevoked("User {UserId} revoked a token.", userId, tokenId, new(), userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnTokenReuse without metadata should log message with correct values")] + public void LogAuthnTokenReuse_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var tokenId = "xyc-abc-123-gfk"; + + var expectedEvent = $"authn_token_reuse:{userId},{tokenId}"; + var expectedMessage = $"User {userId} reused a token."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnTokenReuse("User {UserId} reused a token.", userId, tokenId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthnTokenReuse with metadata should log message with correct values")] + public void LogAuthnTokenReuse_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var tokenId = "xyc-abc-123-gfk"; + + var expectedEvent = $"authn_token_reuse:{userId},{tokenId}"; + var expectedMessage = $"User {userId} reused a token."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnTokenReuse("User {UserId} reused a token.", userId, tokenId, new(), userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LognAuthnTokenDelete without metadata should log message with correct values")] + public void LogAuthnTokenDelete_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_token_delete:{userId}"; + var expectedMessage = $"User {userId} deleted a token."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnTokenDelete("User {UserId} deleted a token.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LognAuthnTokenDelete with metadata should log message with correct values")] + public void LogAuthnTokenDelete_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"authn_token_delete:{userId}"; + var expectedMessage = $"User {userId} deleted a token."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthnTokenDelete("User {UserId} deleted a token.", userId, new(), userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/AuthzTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/AuthzTests.cs new file mode 100644 index 0000000..c8b4e81 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/AuthzTests.cs @@ -0,0 +1,171 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class AuthzTests +{ + [Fact(DisplayName = "LogAuthzFail without metadata should log message with correct values")] + public void LogAuthzFail_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"authz_fail:{userId},{resource}"; + var expectedMessage = $"User {userId} failed authorization."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthzFail("User {UserId} failed authorization.", userId, resource, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthzFail with metadata should log message with correct values")] + public void LogAuthzFail_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"authz_fail:{userId},{resource}"; + var expectedMessage = $"User {userId} failed authorization."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthzFail("User {UserId} failed authorization.", userId, resource, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthzChange without metadata should log message with correct values")] + public void LogAuthzChange_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var from = "read"; + var to = "read_write"; + + var expectedEvent = $"authz_change:{userId},{from},{to}"; + var expectedMessage = $"User {userId} changed authorization from {from} to {to}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthzChange("User {UserId} changed authorization from {From} to {To}.", userId, from, to, userId, from, to); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthzChange with metadata should log message with correct values")] + public void LogAuthzChange_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var from = "read"; + var to = "read_write"; + + var expectedEvent = $"authz_change:{userId},{from},{to}"; + var expectedMessage = $"User {userId} changed authorization from {from} to {to}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthzChange("User {UserId} changed authorization from {From} to {To}.", userId, from, to, userId, from, to); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthzAdmin without metadata should log message with correct values")] + public void LogAuthzAdmin_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var @event = "PrivilegeElevated"; + + var expectedEvent = $"authz_admin:{userId},{@event}"; + var expectedMessage = $"User {userId} performed administrative action {@event}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthzAdmin("User {UserId} performed administrative action {Event}.", userId, @event, userId, @event); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogAuthzAdmin with metadata should log message with correct values")] + public void LogAuthzAdmin_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var @event = "PrivilegeElevated"; + + var expectedEvent = $"authz_admin:{userId},{@event}"; + var expectedMessage = $"User {userId} performed administrative action {@event}."; + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogAuthzAdmin("User {UserId} performed administrative action {Event}.", userId, @event, userId, @event); + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/CryptTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/CryptTests.cs new file mode 100644 index 0000000..3d2c084 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/CryptTests.cs @@ -0,0 +1,113 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class CryptTests +{ + [Fact(DisplayName = "LogCryptDecryptFail without metadata should log message with correct values")] + public void LogCryptDecryptFail_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"crypt_decrypt_fail:{userId}"; + var expectedMessage = $"User {userId} failed to decrypt."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogCryptDecryptFail("User {UserId} failed to decrypt.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogCryptDecryptFail with metadata should log message with correct values")] + public void LogCryptDecryptFail_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"crypt_decrypt_fail:{userId}"; + var expectedMessage = $"User {userId} failed to decrypt."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogCryptDecryptFail("User {UserId} failed to decrypt.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogCryptEncryptFail without metadata should log message with correct values")] + public void LogCryptEncryptFail_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"crypt_encrypt_fail:{userId}"; + var expectedMessage = $"User {userId} failed to encrypt."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogCryptEncryptFail("User {UserId} failed to encrypt.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogCryptEncryptFail with metadata should log message with correct values")] + public void LogCryptEncryptFail_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"crypt_encrypt_fail:{userId}"; + var expectedMessage = $"User {userId} failed to encrypt."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogCryptEncryptFail("User {UserId} failed to encrypt.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/ExcessTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/ExcessTests.cs new file mode 100644 index 0000000..4f8fd4a --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/ExcessTests.cs @@ -0,0 +1,63 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class ExcessTests +{ + [Fact(DisplayName = "LogExcessRateLimitExceeded without metadata should log message with correct values")] + public void LogExcessRateLimitExceeded_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var max = 100; + + var expectedEvent = $"excess_rate_limit_exceeded:{userId},{max}"; + var expectedMessage = $"User {userId} exceeded rate limit of {max}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogExcessRateLimitExceeded("User {UserId} exceeded rate limit of {Max}.", userId, max, userId, max); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogExcessRateLimitExceeded with metadata should log message with correct values")] + public void LogExcessRateLimitExceeded_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var max = 100; + + var expectedEvent = $"excess_rate_limit_exceeded:{userId},{max}"; + var expectedMessage = $"User {userId} exceeded rate limit of {max}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogExcessRateLimitExceeded("User {UserId} exceeded rate limit of {Max}.", userId, max, userId, max); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/InputTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/InputTests.cs new file mode 100644 index 0000000..e4e0d95 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/InputTests.cs @@ -0,0 +1,119 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class InputTests +{ + [Fact(DisplayName = "LogInputValidationFailed without metadata should log message with correct values")] + public void LogInputValidationFailed_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var fields = new List { "Email", "Password" }; + var commaSeparatedFields = string.Join(",", fields); + + var expectedEvent = $"input_validation_failed:({commaSeparatedFields}),{userId}"; + var expectedMessage = $"User {userId} failed input validation for fields: {commaSeparatedFields}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogInputValidationFailed("User {UserId} failed input validation for fields: {Fields}.", fields, userId, userId, commaSeparatedFields); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogInputValidationFailed with metadata should log message with correct values")] + public void LogInputValidationFailed_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var fields = new List { "Email", "Password" }; + var commaSeparatedFields = string.Join(",", fields); + + var expectedEvent = $"input_validation_failed:({commaSeparatedFields}),{userId}"; + var expectedMessage = $"User {userId} failed input validation for fields: {commaSeparatedFields}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogInputValidationFailed("User {UserId} failed input validation for fields: {Fields}.", fields, userId, userId, commaSeparatedFields); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogInputValidationDiscreteFail without metadata should log message with correct values")] + public void LogInputValidationDiscreteFail_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var field = "Email"; + + var expectedEvent = $"input_validation_discrete_fail:{field},{userId}"; + var expectedMessage = $"User {userId} failed input validation for field: {field}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogInputValidationDiscreteFail("User {UserId} failed input validation for field: {Field}.", field, userId, userId, field); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogInputValidationDiscreteFail with metadata should log message with correct values")] + public void LogInputValidationDiscreteFail_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var field = "Email"; + + var expectedEvent = $"input_validation_discrete_fail:{field},{userId}"; + var expectedMessage = $"User {userId} failed input validation for field: {field}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogInputValidationDiscreteFail("User {UserId} failed input validation for field: {Field}.", field, userId, userId, field); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/MaliciousTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/MaliciousTests.cs new file mode 100644 index 0000000..3c95762 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/MaliciousTests.cs @@ -0,0 +1,285 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class MaliciousTests +{ + [Fact(DisplayName = "LogMaliciousExcess404 without metadata should log message with correct values")] + public void LogMaliciousExcess404_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var useragent = "UnitTestAgent/1.0"; + + var expectedEvent = $"malicious_excess_404:{ipAddress},{useragent}"; + var expectedMessage = $"Malicious excess 404 for IP address: {ipAddress}, User-Agent: {useragent}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousExcess404("Malicious excess 404 for IP address: {IpAddress}, User-Agent: {UserAgent}.", ipAddress, useragent, ipAddress, useragent); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousExcess404 with metadata should log message with correct values")] + public void LogMaliciousExcess404_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var useragent = "UnitTestAgent/1.0"; + + var expectedEvent = $"malicious_excess_404:{ipAddress},{useragent}"; + var expectedMessage = $"Malicious excess 404 for IP address: {ipAddress}, User-Agent: {useragent}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousExcess404("Malicious excess 404 for IP address: {IpAddress}, User-Agent: {UserAgent}.", ipAddress, useragent, ipAddress, useragent); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousExtraneous without metadata should log message with correct values")] + public void LogMaliciousExtraneous_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var inputName = "testInput"; + var useragent = "UnitTestAgent/1.0"; + + var expectedEvent = $"malicious_extraneous:{ipAddress},{inputName},{useragent}"; + var expectedMessage = $"Malicious extraneous input for IP address: {ipAddress}, Input Name: {inputName}, User-Agent: {useragent}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousExtraneous("Malicious extraneous input for IP address: {IpAddress}, Input Name: {InputName}, User-Agent: {UserAgent}.", ipAddress, inputName, useragent, ipAddress, inputName, useragent); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousExtraneous with metadata should log message with correct values")] + public void LogMaliciousExtraneous_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var inputName = "testInput"; + var useragent = "UnitTestAgent/1.0"; + + var expectedEvent = $"malicious_extraneous:{ipAddress},{inputName},{useragent}"; + var expectedMessage = $"Malicious extraneous input for IP address: {ipAddress}, Input Name: {inputName}, User-Agent: {useragent}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousExtraneous("Malicious extraneous input for IP address: {IpAddress}, Input Name: {InputName}, User-Agent: {UserAgent}.", ipAddress, inputName, useragent, ipAddress, inputName, useragent); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousAttackTool without metadata should log message with correct values")] + public void LogMaliciousAttackTool_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var toolName = "TestTool"; + var useragent = "UnitTestAgent/1.0"; + + var expectedEvent = $"malicious_attack_tool:{ipAddress},{toolName},{useragent}"; + var expectedMessage = $"Malicious attack tool detected for IP address: {ipAddress}, Tool Name: {toolName}, User-Agent: {useragent}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousAttackTool("Malicious attack tool detected for IP address: {IpAddress}, Tool Name: {ToolName}, User-Agent: {UserAgent}.", ipAddress, toolName, useragent, ipAddress, toolName, useragent); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousAttackTool with metadata should log message with correct values")] + public void LogMaliciousAttackTool_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var toolName = "TestTool"; + var useragent = "UnitTestAgent/1.0"; + + var expectedEvent = $"malicious_attack_tool:{ipAddress},{toolName},{useragent}"; + var expectedMessage = $"Malicious attack tool detected for IP address: {ipAddress}, Tool Name: {toolName}, User-Agent: {useragent}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousAttackTool("Malicious attack tool detected for IP address: {IpAddress}, Tool Name: {ToolName}, User-Agent: {UserAgent}.", ipAddress, toolName, useragent, ipAddress, toolName, useragent); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousCors without metadata should log message with correct values")] + public void LogMaliciousCors_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var useragent = "UnitTestAgent/1.0"; + var referrer = "http://malicious.example.com"; + + var expectedEvent = $"malicious_cors:{ipAddress},{useragent},{referrer}"; + var expectedMessage = $"Malicious CORS detected for IP address: {ipAddress}, User-Agent: {useragent}, Referrer: {referrer}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousCors("Malicious CORS detected for IP address: {IpAddress}, User-Agent: {UserAgent}, Referrer: {Referrer}.", ipAddress, useragent, referrer, ipAddress, useragent, referrer); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousCors with metadata should log message with correct values")] + public void LogMaliciousCors_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var useragent = "UnitTestAgent/1.0"; + var referrer = "http://malicious.example.com"; + + var expectedEvent = $"malicious_cors:{ipAddress},{useragent},{referrer}"; + var expectedMessage = $"Malicious CORS detected for IP address: {ipAddress}, User-Agent: {useragent}, Referrer: {referrer}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousCors("Malicious CORS detected for IP address: {IpAddress}, User-Agent: {UserAgent}, Referrer: {Referrer}.", ipAddress, useragent, referrer, ipAddress, useragent, referrer); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousDirectReference without metadata should log message with correct values")] + public void LogMaliciousDirectReference_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var useragent = "UnitTestAgent/1.0"; + + var expectedEvent = $"malicious_direct_reference:{ipAddress},{useragent}"; + var expectedMessage = $"Malicious direct reference detected for IP address: {ipAddress}, User-Agent: {useragent}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousDirectReference("Malicious direct reference detected for IP address: {IpAddress}, User-Agent: {UserAgent}.", ipAddress, useragent, ipAddress, useragent); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogMaliciousDirectReference with metadata should log message with correct values")] + public void LogMaliciousDirectReference_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var ipAddress = "192.168.1.1"; + var useragent = "UnitTestAgent/1.0"; + + var expectedEvent = $"malicious_direct_reference:{ipAddress},{useragent}"; + var expectedMessage = $"Malicious direct reference detected for IP address: {ipAddress}, User-Agent: {useragent}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogMaliciousDirectReference("Malicious direct reference detected for IP address: {IpAddress}, User-Agent: {UserAgent}.", ipAddress, useragent, ipAddress, useragent); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/PrivilegeTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/PrivilegeTests.cs new file mode 100644 index 0000000..87574d8 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/PrivilegeTests.cs @@ -0,0 +1,67 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class PrivilegeTests +{ + [Fact(DisplayName = "LogPrivilegePermissionsChanged without metadata should log message with correct values")] + public void LogPrivilegePermissionsChanged_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var resource = "TestResource"; + var fromLevel = "User"; + var toLevel = "Admin"; + + var expectedEvent = $"privilege_permissions_changed:{userId},{resource},{fromLevel},{toLevel}"; + var expectedMessage = $"Privilege permissions changed for user: {userId}, resource: {resource}, from level: {fromLevel}, to level: {toLevel}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogPrivilegePermissionsChanged("Privilege permissions changed for user: {UserId}, resource: {Resource}, from level: {FromLevel}, to level: {ToLevel}.", userId, resource, fromLevel, toLevel, userId, resource, fromLevel, toLevel); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogPrivilegePermissionsChanged with metadata should log message with correct values")] + public void LogPrivilegePermissionsChanged_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var resource = "TestResource"; + var fromLevel = "User"; + var toLevel = "Admin"; + + var expectedEvent = $"privilege_permissions_changed:{user},{resource},{fromLevel},{toLevel}"; + var expectedMessage = $"Privilege permissions changed for user: {user}, resource: {resource}, from level: {fromLevel}, to level: {toLevel}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogPrivilegePermissionsChanged("Privilege permissions changed for user: {User}, resource: {Resource}, from level: {FromLevel}, to level: {ToLevel}.", user, resource, fromLevel, toLevel, user, resource, fromLevel, toLevel); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SensitiveTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SensitiveTests.cs new file mode 100644 index 0000000..90e0a4d --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SensitiveTests.cs @@ -0,0 +1,225 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class SensitiveTests +{ + [Fact(DisplayName = "LogSensitiveCreate without metadata should log message with correct values")] + public void LogSensitiveCreate_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"sensitive_create:{userId},{resource}"; + var expectedMessage = $"Sensitive data created for user: {userId}, resource: {resource}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSensitiveCreate("Sensitive data created for user: {UserId}, resource: {Resource}.", userId, resource, userId, resource); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSensitiveCreate with metadata should log message with correct values")] + public void LogSensitiveCreate_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"sensitive_create:{user},{resource}"; + var expectedMessage = $"Sensitive data created for user: {user}, resource: {resource}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSensitiveCreate("Sensitive data created for user: {User}, resource: {Resource}.", user, resource, user, resource); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSensitiveRead without metadata should log message with correct values")] + public void LogSensitiveRead_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"sensitive_read:{userId},{resource}"; + var expectedMessage = $"Sensitive data read for user: {userId}, resource: {resource}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSensitiveRead("Sensitive data read for user: {UserId}, resource: {Resource}.", userId, resource, userId, resource); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSensitiveRead with metadata should log message with correct values")] + public void LogSensitiveRead_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"sensitive_read:{user},{resource}"; + var expectedMessage = $"Sensitive data read for user: {user}, resource: {resource}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSensitiveRead("Sensitive data read for user: {User}, resource: {Resource}.", user, resource, user, resource); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSensitiveUpdate without metadata should log message with correct values")] + public void LogSensitiveUpdate_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"sensitive_update:{userId},{resource}"; + var expectedMessage = $"Sensitive data updated for user: {userId}, resource: {resource}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSensitiveUpdate("Sensitive data updated for user: {UserId}, resource: {Resource}.", userId, resource, userId, resource); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSensitiveUpdate with metadata should log message with correct values")] + public void LogSensitiveUpdate_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"sensitive_update:{user},{resource}"; + var expectedMessage = $"Sensitive data updated for user: {user}, resource: {resource}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSensitiveUpdate("Sensitive data updated for user: {User}, resource: {Resource}.", user, resource, user, resource); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSensitiveDelete without metadata should log message with correct values")] + public void LogSensitiveDelete_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"sensitive_delete:{userId},{resource}"; + var expectedMessage = $"Sensitive data deleted for user: {userId}, resource: {resource}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSensitiveDelete("Sensitive data deleted for user: {UserId}, resource: {Resource}.", userId, resource, userId, resource); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSensitiveDelete with metadata should log message with correct values")] + public void LogSensitiveDelete_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var resource = "TestResource"; + + var expectedEvent = $"sensitive_delete:{user},{resource}"; + var expectedMessage = $"Sensitive data deleted for user: {user}, resource: {resource}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSensitiveDelete("Sensitive data deleted for user: {User}, resource: {Resource}.", user, resource, user, resource); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SequenceTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SequenceTests.cs new file mode 100644 index 0000000..e420cb4 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SequenceTests.cs @@ -0,0 +1,61 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class SequenceTests +{ + [Fact(DisplayName = "LogSequenceFail without metadata should log message with correct values")] + public void LogSequenceFail_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"sequence_fail:{userId}"; + var expectedMessage = $"Sequence failed for user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSequenceFail("Sequence failed for user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSequenceFail with metadata should log message with correct values")] + public void LogSequenceFail_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + + var expectedEvent = $"sequence_fail:{user}"; + var expectedMessage = $"Sequence failed for user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSequenceFail("Sequence failed for user: {User}.", user, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SessionTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SessionTests.cs new file mode 100644 index 0000000..52a1b51 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SessionTests.cs @@ -0,0 +1,219 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class SessionTests +{ + [Fact(DisplayName = "LogSessionCreated without metadata should log message with correct values")] + public void LogSessionCreated_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"session_created:{userId}"; + var expectedMessage = $"Session created for user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSessionCreated("Session created for user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSessionCreated with metadata should log message with correct values")] + public void LogSessionCreated_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + + var expectedEvent = $"session_created:{user}"; + var expectedMessage = $"Session created for user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSessionCreated("Session created for user: {User}.", user, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSessionRenewed without metadata should log message with correct values")] + public void LogSessionRenewed_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"session_renewed:{userId}"; + var expectedMessage = $"Session renewed for user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSessionRenewed("Session renewed for user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSessionRenewed with metadata should log message with correct values")] + public void LogSessionRenewed_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + + var expectedEvent = $"session_renewed:{user}"; + var expectedMessage = $"Session renewed for user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSessionRenewed("Session renewed for user: {User}.", user, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSessionExpired without metadata should log message with correct values")] + public void LogSessionExpired_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var reason = "Inactivity"; + + var expectedEvent = $"session_expired:{userId},{reason}"; + var expectedMessage = $"Session expired for user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSessionExpired("Session expired for user: {UserId}.", userId, reason, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSessionExpired with metadata should log message with correct values")] + public void LogSessionExpired_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var reason = "Inactivity"; + + var expectedEvent = $"session_expired:{userId},{reason}"; + var expectedMessage = $"Session expired for user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSessionExpired("Session expired for user: {User}.", userId, reason, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSessionUseAfterExpire without metadata should log message with correct values")] + public void LogSessionUseAfterExpire_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"session_use_after_expire:{userId}"; + var expectedMessage = $"Session used after expiration for user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSessionUseAfterExpire("Session used after expiration for user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSessionUseAfterExpire with metadata should log message with correct values")] + public void LogSessionUseAfterExpire_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"session_use_after_expire:{userId}"; + var expectedMessage = $"Session used after expiration for user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSessionUseAfterExpire("Session used after expiration for user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Critical, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SystemTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SystemTests.cs new file mode 100644 index 0000000..c6204e7 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/SystemTests.cs @@ -0,0 +1,325 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class SystemTests +{ + [Fact(DisplayName = "LogSysStartup without metadata should log message with correct values")] + public void LogSysStartup_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"sys_startup:{userId}"; + var expectedMessage = $"System started by user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysStartup("System started by user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysStartup with metadata should log message with correct values")] + public void LogSysStartup_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + + var expectedEvent = $"sys_startup:{user}"; + var expectedMessage = $"System started by user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysStartup("System started by user: {User}.", user, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysShutdown without metadata should log message with correct values")] + public void LogSysShutdown_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"sys_shutdown:{userId}"; + var expectedMessage = $"System shutdown by user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysShutdown("System shutdown by user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysShutdown with metadata should log message with correct values")] + public void LogSysShutdown_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + + var expectedEvent = $"sys_shutdown:{user}"; + var expectedMessage = $"System shutdown by user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysShutdown("System shutdown by user: {User}.", user, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysRestart without metadata should log message with correct values")] + public void LogSysRestart_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"sys_restart:{userId}"; + var expectedMessage = $"System restart by user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysRestart("System restart by user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysRestart with metadata should log message with correct values")] + public void LogSysRestart_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + + var expectedEvent = $"sys_restart:{user}"; + var expectedMessage = $"System restart by user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysRestart("System restart by user: {User}.", user, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysCrash without metadata should log message with correct values")] + public void LogSysCrash_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + + var expectedEvent = $"sys_crash:{userId}"; + var expectedMessage = $"System crash by user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysCrash("System crash by user: {UserId}.", userId, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysCrash with metadata should log message with correct values")] + public void LogSysCrash_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + + var expectedEvent = $"sys_crash:{user}"; + var expectedMessage = $"System crash by user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysCrash("System crash by user: {User}.", user, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysMonitorDisabled without metadata should log message with correct values")] + public void LogSysMonitorDisabled_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var monitor = "TestMonitor"; + + var expectedEvent = $"sys_monitor_disabled:{userId},{monitor}"; + var expectedMessage = $"System monitor disabled by user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysMonitorDisabled("System monitor disabled by user: {UserId}.", userId, monitor, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysMonitorDisabled with metadata should log message with correct values")] + public void LogSysMonitorDisabled_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var monitor = "TestMonitor"; + + var expectedEvent = $"sys_monitor_disabled:{user},{monitor}"; + var expectedMessage = $"System monitor disabled by user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysMonitorDisabled("System monitor disabled by user: {User}.", user, monitor, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysMonitorEnabled without metadata should log message with correct values")] + public void LogSysMonitorEnabled_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var monitor = "TestMonitor"; + + var expectedEvent = $"sys_monitor_enabled:{userId},{monitor}"; + var expectedMessage = $"System monitor enabled by user: {userId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysMonitorEnabled("System monitor enabled by user: {UserId}.", userId, monitor, userId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogSysMonitorEnabled with metadata should log message with correct values")] + public void LogSysMonitorEnabled_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var monitor = "TestMonitor"; + + var expectedEvent = $"sys_monitor_enabled:{user},{monitor}"; + var expectedMessage = $"System monitor enabled by user: {user}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogSysMonitorEnabled("System monitor enabled by user: {User}.", user, monitor, user); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/UploadTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/UploadTests.cs new file mode 100644 index 0000000..a9daeea --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/UploadTests.cs @@ -0,0 +1,235 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class UploadTests +{ + [Fact(DisplayName = "LogUploadComplete without metadata should log message with correct values")] + public void LogUploadComplete_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var fileName = "TestFile.txt"; + var fileType = "text/plain"; + + var expectedEvent = $"upload_complete:{userId},{fileName},{fileType}"; + var expectedMessage = $"Upload complete for file: {fileName}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUploadComplete("Upload complete for file: {FileName}.", userId, fileName, fileType, fileName); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUploadComplete with metadata should log message with correct values")] + public void LogUploadComplete_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var fileName = "TestFile.txt"; + var fileType = "text/plain"; + + var expectedEvent = $"upload_complete:{user},{fileName},{fileType}"; + var expectedMessage = $"Upload complete for file: {fileName}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUploadComplete("Upload complete for file: {FileName}.", user, fileName, fileType, fileName); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUploadStored without metadata should log message with correct values")] + public void LogUploadStored_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var from = "/temp/uploads"; + var to = "/permanent/storage"; + + var expectedEvent = $"upload_stored:{userId},{from},{to}"; + var expectedMessage = $"Upload stored from {from} to {to}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUploadStored("Upload stored from {From} to {To}.", userId, from, to, from, to); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUploadStored with metadata should log message with correct values")] + public void LogUploadStored_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var from = "/temp/uploads"; + var to = "/permanent/storage"; + + var expectedEvent = $"upload_stored:{user},{from},{to}"; + var expectedMessage = $"Upload stored from {from} to {to}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUploadStored("Upload stored from {From} to {To}.", user, from, to, from, to); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUploadValidation without metadata should log message with correct values")] + public void LogUploadValidation_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var fileName = "TestFile.txt"; + var validationType = "signature"; + var result = "passed"; + var logLevel = LogLevel.Information; + + var expectedEvent = $"upload_validation:{userId},{fileName},{validationType},{result}"; + var expectedMessage = $"Upload validation for {fileName} with type {validationType} resulted in {result}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUploadValidation("Upload validation for {FileName} with type {ValidationType} resulted in {Result}.", userId, fileName, validationType, result, logLevel, fileName, validationType, result); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(logLevel, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUploadValidation with metadata should log message with correct values")] + public void LogUploadValidation_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var fileName = "TestFile.txt"; + var validationType = "signature"; + var result = "passed"; + var logLevel = LogLevel.Information; + + var expectedEvent = $"upload_validation:{user},{fileName},{validationType},{result}"; + var expectedMessage = $"Upload validation for {fileName} with type {validationType} resulted in {result}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUploadValidation("Upload validation for {FileName} with type {ValidationType} resulted in {Result}.", user, fileName, validationType, result, logLevel, fileName, validationType, result); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(logLevel, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUploadDelete without metadata should log message with correct values")] + public void LogUploadDelete_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var fileId = "File123"; + + var expectedEvent = $"upload_delete:{userId},{fileId}"; + var expectedMessage = $"Upload deleted for file {fileId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUploadDelete("Upload deleted for file {FileId}.", userId, fileId, fileId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUploadDelete with metadata should log message with correct values")] + public void LogUploadDelete_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var fileId = "File123"; + + var expectedEvent = $"upload_delete:{user},{fileId}"; + var expectedMessage = $"Upload deleted for file {fileId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUploadDelete("Upload deleted for file {FileId}.", user, fileId, fileId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/UserTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/UserTests.cs new file mode 100644 index 0000000..22ff636 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/Partials/UserTests.cs @@ -0,0 +1,249 @@ +using ByteGuard.SecurityLogger.Configuration; +using ByteGuard.SecurityLogger.Tests.Unit.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit.Partials; + +public class UserTests +{ + [Fact(DisplayName = "LogUserCreated without metadata should log message with correct values")] + public void LogUserCreated_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var newUserId = "NewTestUser"; + var attributes = new Dictionary> + { + { "role", new[] { "user" } }, + { "department", new[] { "hr" } } + }; + var expectedAttributes = "role:user,department:hr"; + + var expectedEvent = $"user_created:{userId},{newUserId},{expectedAttributes}"; + var expectedMessage = $"User created with ID {newUserId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUserCreated("User created with ID {NewUserId}.", userId, newUserId, attributes, newUserId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUserCreated with metadata should log message with correct values")] + public void LogUserCreated_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var newUserId = "NewTestUser"; + var attributes = new Dictionary> + { + { "role", new[] { "user" } }, + { "department", new[] { "hr" } } + }; + var expectedAttributes = "role:user,department:hr"; + + var expectedEvent = $"user_created:{user},{newUserId},{expectedAttributes}"; + var expectedMessage = $"User created with ID {newUserId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUserCreated("User created with ID {NewUserId}.", user, newUserId, attributes, newUserId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUserUpdated without metadata should log message with correct values")] + public void LogUserUpdated_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var onUserId = "NewTestUser"; + var attributes = new Dictionary> + { + { "role", new[] { "admin" } }, + { "department", new[] { "hr" } } + }; + var expectedAttributes = "role:admin,department:hr"; + + var expectedEvent = $"user_updated:{userId},{onUserId},{expectedAttributes}"; + var expectedMessage = $"User updated with ID {onUserId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUserUpdated("User updated with ID {OnUserId}.", userId, onUserId, attributes, onUserId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUserUpdated with metadata should log message with correct values")] + public void LogUserUpdated_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var onUserId = "NewTestUser"; + var attributes = new Dictionary> + { + { "role", new[] { "admin" } }, + { "department", new[] { "hr" } } + }; + var expectedAttributes = "role:admin,department:hr"; + + var expectedEvent = $"user_updated:{user},{onUserId},{expectedAttributes}"; + var expectedMessage = $"User updated with ID {onUserId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUserUpdated("User updated with ID {OnUserId}.", user, onUserId, attributes, onUserId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUserArchived without metadata should log message with correct values")] + public void LogUserArchived_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var onUserId = "NewTestUser"; + + var expectedEvent = $"user_archived:{userId},{onUserId}"; + var expectedMessage = $"User archived with ID {onUserId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUserArchived("User archived with ID {OnUserId}.", userId, onUserId, onUserId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUserArchived with metadata should log message with correct values")] + public void LogUserArchived_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var onUserId = "NewTestUser"; + + var expectedEvent = $"user_archived:{user},{onUserId}"; + var expectedMessage = $"User archived with ID {onUserId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUserArchived("User archived with ID {OnUserId}.", user, onUserId, onUserId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUserDeleted without metadata should log message with correct values")] + public void LogUserDeleted_WithoutMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var userId = "TestUser"; + var onUserId = "NewTestUser"; + + var expectedEvent = $"user_deleted:{userId},{onUserId}"; + var expectedMessage = $"User deleted with ID {onUserId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUserDeleted("User deleted with ID {OnUserId}.", userId, onUserId, onUserId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } + + [Fact(DisplayName = "LogUserDeleted with metadata should log message with correct values")] + public void LogUserDeleted_WithMetadata_ShouldLogMessageWithCorrectValues() + { + // Arrange + var user = "TestUser"; + var onUserId = "NewTestUser"; + + var expectedEvent = $"user_deleted:{user},{onUserId}"; + var expectedMessage = $"User deleted with ID {onUserId}."; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.LogUserDeleted("User deleted with ID {OnUserId}.", user, onUserId, onUserId); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.GetScopeDictionary(); + + Assert.Equal(LogLevel.Warning, record.Level); + Assert.Equal(expectedMessage, record.Message); + Assert.Contains("Event", scope!); + Assert.Equal(expectedEvent, scope!["Event"]); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/SecurityLoggerFactoryExtensions.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/SecurityLoggerFactoryExtensions.cs new file mode 100644 index 0000000..e5bd5da --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/SecurityLoggerFactoryExtensions.cs @@ -0,0 +1,41 @@ +using ByteGuard.SecurityLogger.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace ByteGuard.SecurityLogger.Tests.Unit; + +public class SecurityLoggerFactoryExtensions +{ + [Fact(DisplayName = "AsSecurityLogger returns SecurityLogger instance")] + public void AsSecurityLogger_ReturnsSecurityLoggerInstance() + { + // Arrange + var fakeLogger = new FakeLogger(); + var configuration = new SecurityLoggerConfiguration { AppId = "TestApp" }; + + // Act + var result = fakeLogger.AsSecurityLogger(configuration); + + // Assert + Assert.IsType(result); + } + + [Fact(DisplayName = "AsSecurityLogger returns a SecurityLogger that logs message to the correct ILogger")] + public void AsSecurityLogger_ReturnsSecurityLoggerThatLogsMessageToCorrectILogger() + { + // Arrange + var expectedEvent = "TestEvent"; + + var fakeLogger = new FakeLogger(); + var configuration = new SecurityLoggerConfiguration { AppId = "TestApp" }; + var securityLogger = fakeLogger.AsSecurityLogger(configuration); + + // Act + securityLogger.Log(expectedEvent, LogLevel.Information, "Test message", new SecurityEventMetadata()); + + // Assert + var record = fakeLogger.Collector.GetSnapshot().Single(); + + Assert.Equal("Test message", record.Message); + } +} diff --git a/tests/ByteGuard.SecurityLogger.Tests.Unit/SecurityLoggerTests.cs b/tests/ByteGuard.SecurityLogger.Tests.Unit/SecurityLoggerTests.cs new file mode 100644 index 0000000..1e1e406 --- /dev/null +++ b/tests/ByteGuard.SecurityLogger.Tests.Unit/SecurityLoggerTests.cs @@ -0,0 +1,172 @@ +using ByteGuard.SecurityLogger.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using NSubstitute; + +namespace ByteGuard.SecurityLogger.Tests.Unit; + +public class SecurityLoggerTests +{ + [Fact(DisplayName = "Constructor should throw ArgumentNullException when ILogger is null")] + public void Constructor_ThrowsArgumentNullException_WhenILoggerIsNull() + { + // Arrange + ILogger logger = null!; + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + + // Act & Assert + Assert.Throws(() => new SecurityLogger(logger, configuration)); + } + + [Fact(DisplayName = "Constructor should throw ArgumentNullException when SecurityLoggerConfiguration is null")] + public void Constructor_ThrowsArgumentNullException_WhenSecurityLoggerConfigurationIsNull() + { + // Arrange + ILogger logger = Substitute.For(); + SecurityLoggerConfiguration configuration = null!; + + // Act & Assert + Assert.Throws(() => new SecurityLogger(logger, configuration)); + } + + [Fact(DisplayName = "Constructor should throw ArgumentException when SecurityLoggerConfiguration is invalid")] + public void Constructor_ThrowsArgumentException_WhenSecurityLoggerConfigurationIsInvalid() + { + // Arrange + ILogger logger = Substitute.For(); + SecurityLoggerConfiguration configuration = new() { AppId = null! }; + + // Act & Assert + Assert.Throws(() => new SecurityLogger(logger, configuration)); + } + + [Fact(DisplayName = "Log should log message with correct values")] + public void Log_ShouldLogMessageWithCorrectValues() + { + // Arrange + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp" }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.Log("TestEvent", LogLevel.Information, "Test message", new SecurityEventMetadata()); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + + Assert.Equal(LogLevel.Information, record.Level); + Assert.Equal("Test message", record.Message); + } + + [Fact(DisplayName = "Log should include AppId and Event in scope properties")] + public void Log_ShouldIncludeAppIdAndEventInScopeProperties() + { + // Arrange + var expectedAppId = "TestApp"; + var expectedEvent = "TestEvent"; + + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = expectedAppId }; + SecurityLogger securityLogger = new(logger, configuration); + + // Act + securityLogger.Log(expectedEvent, LogLevel.Information, "Test message", new SecurityEventMetadata()); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scope = record.Scopes.FirstOrDefault(scope => + { + var dict = scope as IReadOnlyDictionary; + return dict != null && dict.ContainsKey("AppId") && dict.ContainsKey("Event"); + }); + + Assert.Contains("AppId", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedAppId, ((IReadOnlyDictionary)scope!)["AppId"]); + Assert.Contains("Event", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedEvent, ((IReadOnlyDictionary)scope!)["Event"]); + } + + [Fact(DisplayName = "Log should populate properties from SecurityEventMetadata")] + public void Log_ShouldPopulatePropertiesFromSecurityEventMetadata() + { + // Arrange + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp", DisableSourceIpLogging = false }; + SecurityLogger securityLogger = new(logger, configuration); + + var expectedMetadata = new SecurityEventMetadata + { + UserAgent = "TestUserAgent", + SourceIp = "1.1.1.1", + HostIp = "2.2.2.2", + Hostname = "TestHost", + Protocol = "HTTPS", + Port = "443", + RequestUri = "/test/uri", + RequestMethod = "GET", + Region = "TestRegion", + Geo = "TestGeo" + }; + + // Act + securityLogger.Log("TestEvent", LogLevel.Information, "Test message", expectedMetadata); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scopes = record.Scopes; + var scope = scopes.FirstOrDefault(scope => + { + var dict = scope as IReadOnlyDictionary; + return dict != null && dict.ContainsKey("AppId") && dict.ContainsKey("Event"); + }); + + Assert.Contains("UserAgent", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.UserAgent, ((IReadOnlyDictionary)scope!)["UserAgent"]); + Assert.Contains("SourceIp", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.SourceIp, ((IReadOnlyDictionary)scope!)["SourceIp"]); + Assert.Contains("HostIp", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.HostIp, ((IReadOnlyDictionary)scope!)["HostIp"]); + Assert.Contains("Hostname", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.Hostname, ((IReadOnlyDictionary)scope!)["Hostname"]); + Assert.Contains("Protocol", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.Protocol, ((IReadOnlyDictionary)scope!)["Protocol"]); + Assert.Contains("Port", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.Port, ((IReadOnlyDictionary)scope!)["Port"]); + Assert.Contains("RequestUri", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.RequestUri, ((IReadOnlyDictionary)scope!)["RequestUri"]); + Assert.Contains("RequestMethod", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.RequestMethod, ((IReadOnlyDictionary)scope!)["RequestMethod"]); + Assert.Contains("Region", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.Region, ((IReadOnlyDictionary)scope!)["Region"]); + Assert.Contains("Geo", (IReadOnlyDictionary)scope!); + Assert.Equal(expectedMetadata.Geo, ((IReadOnlyDictionary)scope!)["Geo"]); + } + + [Fact(DisplayName = "Log should not add SourceIp property when DisableSourceIpLogging is true")] + public void Log_DisableSourceIpLoggingIsTrue_ShouldNotAddSourceIpProperty() + { + // Arrange + var logger = new FakeLogger(); + SecurityLoggerConfiguration configuration = new() { AppId = "TestApp", DisableSourceIpLogging = true }; + SecurityLogger securityLogger = new(logger, configuration); + + var expectedMetadata = new SecurityEventMetadata + { + SourceIp = "1.1.1.1" + }; + + // Act + securityLogger.Log("TestEvent", LogLevel.Information, "Test message", expectedMetadata); + + // Assert + var record = logger.Collector.GetSnapshot().Single(); + var scopes = record.Scopes; + var scope = scopes.FirstOrDefault(scope => + { + var dict = scope as IReadOnlyDictionary; + return dict != null && dict.ContainsKey("AppId") && dict.ContainsKey("Event"); + }); + + Assert.DoesNotContain("SourceIp", (IReadOnlyDictionary)scope!); + } +}