Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Version.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<VersionPrefix>0.1.0</VersionPrefix>
<VersionPrefix>1.0.0</VersionPrefix>
</PropertyGroup>
</Project>
144 changes: 143 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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._
26 changes: 26 additions & 0 deletions src/ByteGuard.SecurityLogger/Builders/EventLabelBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace ByteGuard.SecurityLogger;

/// <summary>
/// Combines events and event arguments into event strings.
/// </summary>
public static class EventLabelBuilder
{
/// <summary>
/// Build an event string from the given event name and event arguments.
/// </summary>
/// <param name="eventName">Event name.</param>
/// <param name="eventArgs">Event arguments.</param>
/// <returns>An appropriate event string.</returns>
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}";
}
}
12 changes: 10 additions & 2 deletions src/ByteGuard.SecurityLogger/ByteGuard.SecurityLogger.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Authors>ByteGuard Contributors, detilium</Authors>
<Description>OWASP Logging Vocabulary for .NET — ILogger extension methods that emit consistent, structured security events across any logging provider (Serilog, NLog, etc.)..</Description>
<Description>OWASP Logging Vocabulary for .NET brings a lightweight ILogger wrapper that emits consistent, structured security events across any logging provider (NLog, Serilog, etc.).</Description>
<PackageProjectUrl>https://github.com/ByteGuard-HQ/byteguard-security-logger</PackageProjectUrl>
<RepositoryUrl>https://github.com/ByteGuard-HQ/byteguard-security-logger</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand All @@ -19,4 +19,12 @@
<None Include="..\..\assets\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
</ItemGroup>

<ItemGroup>
<Folder Include="Configuration/" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace ByteGuard.SecurityLogger.Configuration;

/// <summary>
/// Class used to validate a given security logger configuration instance.
/// </summary>
public static class ConfigurationValidator
{
/// <summary>
/// Validate configuration and throw exceptions if invalid.
/// </summary>
/// <param name="configuration">Configuration instance to validate.</param>
/// <exception cref="ArgumentNullException">Throw if any required objects on the configuration object is <c>null</c>, or if the configuration object itself is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown if any of the configuration values are invalid.</exception>
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));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ByteGuard.SecurityLogger.Configuration;

/// <summary>
/// Configuration for the security logger.
/// </summary>
public class SecurityLoggerConfiguration
{
/// <summary>
/// App identifier.
/// </summary>
public string AppId { get; set; } = default!;

/// <summary>
/// Whether to disable logging of source IP addresses. Defaults to <c>true</c>.
/// </summary>
public bool DisableSourceIpLogging { get; set; } = true;
}
47 changes: 47 additions & 0 deletions src/ByteGuard.SecurityLogger/Enrichers/PropertyEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using ByteGuard.SecurityLogger.Configuration;

namespace ByteGuard.SecurityLogger.Enrichers;

internal static class PropertiesEnricher
{
/// <summary>
/// Populate the properties from the given metadata instance.
/// </summary>
/// <param name="properties">Properties to populate.</param>
/// <param name="metadata">Metadata instance.</param>
/// <param name="configuration">Security logger configuration.</param>
internal static void PopulatePropertiesFromMetadata(Dictionary<string, object?> 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);
}
}
66 changes: 66 additions & 0 deletions src/ByteGuard.SecurityLogger/LoggingVocabulary.cs
Original file line number Diff line number Diff line change
@@ -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";
}
Loading
Loading