Skip to content

Commit d299c9f

Browse files
authored
Initial release
1 parent 617f48f commit d299c9f

43 files changed

Lines changed: 5515 additions & 5 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Directory.Version.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
22
<PropertyGroup>
3-
<VersionPrefix>0.1.0</VersionPrefix>
3+
<VersionPrefix>1.0.0</VersionPrefix>
44
</PropertyGroup>
55
</Project>

README.md

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,147 @@
11
# ByteGuard.SecurityLogger ![NuGet Version](https://img.shields.io/nuget/v/ByteGuard.SecurityLogger)
22

3-
`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.
3+
`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.
44

55
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.
6+
7+
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.
8+
9+
## Features
10+
11+
- ✅ OWASP-aligned security event vocabulary
12+
- ✅ Structured logging via `ILogger` scopes/properties
13+
- ✅ Works with any `Microsoft.Extensions.ILogger` provider (_NLog, Serilog, etc._)
14+
15+
## Getting Started
16+
17+
### Installation
18+
19+
This package is published and installed via [NuGet](https://www.nuget.org/packages/ByteGuard.SecurityLogger).
20+
21+
Reference the package in your project:
22+
23+
```bash
24+
dotnet add package ByteGuard.SecurityLogger
25+
```
26+
27+
## Usage
28+
29+
Instantiate a new `SecurityLogger` instance using either the constructor or the `ILogger` extensions: `AsSecurityLogger()`.
30+
31+
```csharp
32+
ILogger logger = /* resolve or create ILogger */
33+
34+
var configuration = new SecurityLoggerConfiguration
35+
{
36+
AppId = "MyApp"
37+
}
38+
39+
// Using constructor
40+
var securityLogger = new SecurityLogger(logger, configuration);
41+
42+
// Using ILogger extensions
43+
var securityLogger = logger.AsSecurityLogger(configuration);
44+
```
45+
46+
Log your security events:
47+
48+
```csharp
49+
var user = //...
50+
51+
securityLogger.AuthnLoginSuccess(
52+
"User {UserId} successfully logged in.",
53+
userId: user.Id,
54+
args: user.Id
55+
)
56+
```
57+
58+
## API Design
59+
60+
`ByteGuard.SecurityLogger` implements the **full OWASP Logging Vocabulary**: every event type defined by OWASP exists as a corresponding method on `SecurityLogger`.
61+
62+
### One method per event (plus an overload with metadata)
63+
64+
For each OWASP event type, `SecurityLogger` exposes two overloads:
65+
66+
1. A minimal overload for logging the event with just the event label parameters.
67+
68+
```csharp
69+
securityLogger.Log{event}(
70+
string message,
71+
/* event label arguments (varies by event) */,
72+
params object?[] args
73+
)
74+
```
75+
76+
2. An overload that additionally accepts a `SecurityEventMetadata` object for richer, OWASP-recommended context (_client IP, hostname, request URI, etc._).
77+
78+
```csharp
79+
securityLogger.Log{event}(
80+
string message,
81+
/* event label arguments (varies by event) */,
82+
SecurityEventMetadata metadata,
83+
params object?[] args
84+
)
85+
```
86+
87+
### Parameter order (always the same)
88+
89+
| Parameter | Description |
90+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91+
| `message` | The human readable log message (_typically a message template_) |
92+
| 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`. |
93+
| `metadata` | Additional structured context recommended by OWASP (source IP, host, request URI, etc.) (_Only in the metadata method overload_) |
94+
| `args` | The message template arguments from the 1st parameters |
95+
96+
### Example
97+
98+
If an event label requires a `userId`, the call becomes:
99+
100+
```csharp
101+
// Providing user ID produces label: authn_login_success:userOne
102+
var userId = "userOne";
103+
securityLogger.LogAuthnLoginSuccess(
104+
"User {UserId} logged in successfully from {Ip}", // Message template
105+
userId, // Label parameters
106+
userId, ip); // Template args
107+
108+
// Without providing user ID produces label: authn_login_success
109+
securityLogger.LogAuthnLoginSuccess(
110+
"User {UserId} logged in successfully from {Ip}", // Message template
111+
null, // Label parameters
112+
userId, ip); // Template args
113+
```
114+
115+
If you want to add OWASP-style context, use the metadata overload:
116+
117+
```csharp
118+
securityLogger.LogAuthnLoginSuccess(
119+
"User {UserId} logged in successfully from {Ip}", // Message template
120+
userId, // Label parameters
121+
new SecurityEventMetadata // Event metadata
122+
{
123+
SourceIp = ip,
124+
Hostname = host,
125+
RequestUri = requestUri
126+
},
127+
userId, ip); // Template args
128+
```
129+
130+
> ℹ️ **Note:** The exact label arguments vary per event type, based on the OWASP Logging Vocabulary definition.
131+
132+
## Configuration
133+
134+
The `SecurityLogger` supports the following configurations:
135+
136+
| Configuration | Required | Default | Description |
137+
| ------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
138+
| `AppId` | Yes | N/A | Application identifier added to the log message, to ensure logs are easy to find for the given application |
139+
| `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_) |
140+
141+
## Supported events
142+
143+
All supported events can be seen in the [WIKI](https://github.com/ByteGuard-HQ/byteguard-security-logger/wiki/Supported-events)
144+
145+
## License
146+
147+
_ByteGuard.SecurityLogger is Copyright © ByteGuard Contributors - Provided under the MIT license._
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace ByteGuard.SecurityLogger;
2+
3+
/// <summary>
4+
/// Combines events and event arguments into event strings.
5+
/// </summary>
6+
public static class EventLabelBuilder
7+
{
8+
/// <summary>
9+
/// Build an event string from the given event name and event arguments.
10+
/// </summary>
11+
/// <param name="eventName">Event name.</param>
12+
/// <param name="eventArgs">Event arguments.</param>
13+
/// <returns>An appropriate event string.</returns>
14+
public static string BuildEventString(string eventName, params string?[] eventArgs)
15+
{
16+
if (eventArgs is null || eventArgs.Length == 0)
17+
return eventName;
18+
19+
var commaSeparatedEventArgs = string.Join(",", eventArgs.Where(arg => !string.IsNullOrEmpty(arg)));
20+
21+
if (string.IsNullOrWhiteSpace(commaSeparatedEventArgs))
22+
return eventName;
23+
24+
return $"{eventName}:{commaSeparatedEventArgs}";
25+
}
26+
}

src/ByteGuard.SecurityLogger/ByteGuard.SecurityLogger.csproj

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.0</TargetFrameworks>
55
<Authors>ByteGuard Contributors, detilium</Authors>
6-
<Description>OWASP Logging Vocabulary for .NET — ILogger extension methods that emit consistent, structured security events across any logging provider (Serilog, NLog, etc.)..</Description>
6+
<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>
77
<PackageProjectUrl>https://github.com/ByteGuard-HQ/byteguard-security-logger</PackageProjectUrl>
88
<RepositoryUrl>https://github.com/ByteGuard-HQ/byteguard-security-logger</RepositoryUrl>
99
<RepositoryType>git</RepositoryType>
@@ -19,4 +19,12 @@
1919
<None Include="..\..\assets\icon.png" Pack="true" PackagePath="\" />
2020
</ItemGroup>
2121

22+
<ItemGroup>
23+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<Folder Include="Configuration/" />
28+
</ItemGroup>
29+
2230
</Project>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace ByteGuard.SecurityLogger.Configuration;
2+
3+
/// <summary>
4+
/// Class used to validate a given security logger configuration instance.
5+
/// </summary>
6+
public static class ConfigurationValidator
7+
{
8+
/// <summary>
9+
/// Validate configuration and throw exceptions if invalid.
10+
/// </summary>
11+
/// <param name="configuration">Configuration instance to validate.</param>
12+
/// <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>
13+
/// <exception cref="ArgumentException">Thrown if any of the configuration values are invalid.</exception>
14+
public static void ThrowIfInvalid(SecurityLoggerConfiguration configuration)
15+
{
16+
if (configuration == null)
17+
{
18+
throw new ArgumentNullException(nameof(configuration), "Configuration cannot be null.");
19+
}
20+
21+
if (string.IsNullOrWhiteSpace(configuration.AppId))
22+
{
23+
throw new ArgumentException("AppId cannot be null or empty.", nameof(configuration.AppId));
24+
}
25+
}
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace ByteGuard.SecurityLogger.Configuration;
2+
3+
/// <summary>
4+
/// Configuration for the security logger.
5+
/// </summary>
6+
public class SecurityLoggerConfiguration
7+
{
8+
/// <summary>
9+
/// App identifier.
10+
/// </summary>
11+
public string AppId { get; set; } = default!;
12+
13+
/// <summary>
14+
/// Whether to disable logging of source IP addresses. Defaults to <c>true</c>.
15+
/// </summary>
16+
public bool DisableSourceIpLogging { get; set; } = true;
17+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using ByteGuard.SecurityLogger.Configuration;
2+
3+
namespace ByteGuard.SecurityLogger.Enrichers;
4+
5+
internal static class PropertiesEnricher
6+
{
7+
/// <summary>
8+
/// Populate the properties from the given metadata instance.
9+
/// </summary>
10+
/// <param name="properties">Properties to populate.</param>
11+
/// <param name="metadata">Metadata instance.</param>
12+
/// <param name="configuration">Security logger configuration.</param>
13+
internal static void PopulatePropertiesFromMetadata(Dictionary<string, object?> properties, SecurityEventMetadata? metadata, SecurityLoggerConfiguration configuration)
14+
{
15+
if (metadata is null) return;
16+
17+
if (!string.IsNullOrWhiteSpace(metadata.UserAgent))
18+
properties.Add("UserAgent", metadata.UserAgent);
19+
20+
if (!string.IsNullOrWhiteSpace(metadata.SourceIp) && !configuration.DisableSourceIpLogging)
21+
properties.Add("SourceIp", metadata.SourceIp);
22+
23+
if (!string.IsNullOrWhiteSpace(metadata.HostIp))
24+
properties.Add("HostIp", metadata.HostIp);
25+
26+
if (!string.IsNullOrWhiteSpace(metadata.Hostname))
27+
properties.Add("Hostname", metadata.Hostname);
28+
29+
if (!string.IsNullOrWhiteSpace(metadata.Protocol))
30+
properties.Add("Protocol", metadata.Protocol);
31+
32+
if (!string.IsNullOrWhiteSpace(metadata.Port))
33+
properties.Add("Port", metadata.Port);
34+
35+
if (!string.IsNullOrWhiteSpace(metadata.RequestUri))
36+
properties.Add("RequestUri", metadata.RequestUri);
37+
38+
if (!string.IsNullOrWhiteSpace(metadata.RequestMethod))
39+
properties.Add("RequestMethod", metadata.RequestMethod);
40+
41+
if (!string.IsNullOrWhiteSpace(metadata.Region))
42+
properties.Add("Region", metadata.Region);
43+
44+
if (!string.IsNullOrWhiteSpace(metadata.Geo))
45+
properties.Add("Geo", metadata.Geo);
46+
}
47+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace ByteGuard.SecurityLogger;
2+
3+
internal static class LoggingVocabulary
4+
{
5+
internal const string AuthnLoginSuccess = "authn_login_success";
6+
internal const string AuthnLoginSuccessAfterFail = "authn_login_successafterfail";
7+
internal const string AuthnLoginFail = "authn_login_fail";
8+
internal const string AuthnLoginFailMax = "authn_login_fail_max";
9+
internal const string AuthnLoginLock = "authn_login_lock";
10+
internal const string AuthnPasswordChange = "authn_password_change";
11+
internal const string AuthnPasswordChangeFail = "authn_password_change_fail";
12+
internal const string AuthnImpossibleTravel = "authn_impossible_travel";
13+
internal const string AuthnTokenCreated = "authn_token_created";
14+
internal const string AuthnTokenRevoked = "authn_token_revoked";
15+
internal const string AuthnTokenReuse = "authn_token_reuse";
16+
internal const string AuthnTokenDelete = "authn_token_delete";
17+
18+
internal const string AuthzFail = "authz_fail";
19+
internal const string AuthzChange = "authz_change";
20+
internal const string AuthzAdmin = "authz_admin";
21+
22+
internal const string CryptDecryptFail = "crypt_decrypt_fail";
23+
internal const string CryptEncryptFail = "crypt_encrypt_fail";
24+
25+
internal const string ExcessRateLimitExceeded = "excess_rate_limit_exceeded";
26+
27+
internal const string UploadComplete = "upload_complete";
28+
internal const string UploadStored = "upload_stored";
29+
internal const string UploadValidation = "upload_validation";
30+
internal const string UploadDelete = "upload_delete";
31+
32+
internal const string InputValidationFailed = "input_validation_failed";
33+
internal const string InputValidationDiscreteFail = "input_validation_discrete_fail";
34+
35+
internal const string MaliciousExcess404 = "malicious_excess_404";
36+
internal const string MaliciousExtraneous = "malicious_extraneous";
37+
internal const string MaliciousAttackTool = "malicious_attack_tool";
38+
internal const string MaliciousCors = "malicious_cors";
39+
internal const string MaliciousDirectReference = "malicious_direct_reference";
40+
41+
internal const string PrivilegePermissionsChanged = "privilege_permissions_changed";
42+
43+
internal const string SensitiveCreate = "sensitive_create";
44+
internal const string SensitiveRead = "sensitive_read";
45+
internal const string SensitiveUpdate = "sensitive_update";
46+
internal const string SensitiveDelete = "sensitive_delete";
47+
48+
internal const string SequenceFail = "sequence_fail";
49+
50+
internal const string SessionCreated = "session_created";
51+
internal const string SessionRenewed = "session_renewed";
52+
internal const string SessionExpired = "session_expired";
53+
internal const string SessionUseAfterExpire = "session_use_after_expire";
54+
55+
internal const string SysStartup = "sys_startup";
56+
internal const string SysShutdown = "sys_shutdown";
57+
internal const string SysRestart = "sys_restart";
58+
internal const string SysCrash = "sys_crash";
59+
internal const string SysMonitorDisabled = "sys_monitor_disabled";
60+
internal const string SysMonitorEnabled = "sys_monitor_enabled";
61+
62+
internal const string UserCreated = "user_created";
63+
internal const string UserUpdated = "user_updated";
64+
internal const string UserArchived = "user_archived";
65+
internal const string UserDeleted = "user_deleted";
66+
}

0 commit comments

Comments
 (0)