mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-12 21:57:19 -04:00
Account.ToString() returned "id - locale", so interpolating an account or
logging a non-destructured {Account} published the address. It now returns
the masked entry, with a DebuggerDisplay keeping the real values visible
while debugging. Nothing in the UI relied on it: both scan dialogs build
their own labels.
For structured logging, an ILogMasked type is reduced to its masked entry
by a destructuring policy, which covers the {@DebugInfo} shape most of
Libation's logging uses. And DecryptKey - the activation bytes - is now a
SecretString, so it has no plaintext for a reflective dump to find at all.
Its JSON stays the bare string it always was, so existing settings files
load unchanged.
A registered policy that nobody notices is missing protects nothing, so
the tests write through a logger built by ConfigureLogging itself rather
than a hand-made one. Deleting either registration fails them: the masked
object comes out whole, and a destructured secret renders as
{"HasValue":true} instead of its length.
The contribute guide now states the rule, since the reason for all of
this is invisible from the code alone: log files get attached to public
issues, so treat what goes in them as published.
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
110 lines
3.6 KiB
C#
110 lines
3.6 KiB
C#
using Dinah.Core.Security;
|
|
using LibationFileManager;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
using Newtonsoft.Json.Linq;
|
|
using Serilog;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
namespace SerilogConfigurationTests;
|
|
|
|
/// <summary>
|
|
/// Builds the logger the way Libation does and writes through it, rather than testing the destructuring policy
|
|
/// on its own: the policy only protects anything if <see cref="Configuration.ConfigureLogging"/> actually
|
|
/// registers it, and a missing registration is silent.
|
|
/// </summary>
|
|
[TestClass]
|
|
[DoNotParallelize]
|
|
public class MaskedLogEntryLoggingTests
|
|
{
|
|
private const string Secret = "jade@example.com";
|
|
private const string Masked = "AccountId=j[...]e|Locale=us";
|
|
|
|
private string tempDir = string.Empty;
|
|
private ILogger originalLogger = Serilog.Log.Logger;
|
|
|
|
private class MaskedThing : ILogMasked
|
|
{
|
|
public string MaskedLogEntry => Masked;
|
|
public string Address => Secret;
|
|
public override string ToString() => Masked;
|
|
}
|
|
|
|
[TestInitialize]
|
|
public void Initialize()
|
|
{
|
|
originalLogger = Serilog.Log.Logger;
|
|
tempDir = Path.Combine(Path.GetTempPath(), $"libation-masked-log-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempDir);
|
|
}
|
|
|
|
[TestCleanup]
|
|
public void Cleanup()
|
|
{
|
|
Serilog.Log.CloseAndFlush();
|
|
Serilog.Log.Logger = originalLogger;
|
|
Configuration.RestoreSingletonInstance();
|
|
|
|
try
|
|
{
|
|
Directory.Delete(tempDir, recursive: true);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// A leftover temp directory is not worth failing a test over.
|
|
}
|
|
}
|
|
|
|
private string LogThrough(Action<ILogger> write)
|
|
{
|
|
var config = Configuration.CreateMockInstance();
|
|
config.EnsureSerilogConfig();
|
|
|
|
var args = (JObject)((JObject)config.GetObject("Serilog")!).SelectToken("$.WriteTo[0].Args")!;
|
|
args["path"] = Path.Combine(tempDir, "Log.log");
|
|
args["outputTemplate"] = "{Message:lj} {Properties:j}{NewLine}";
|
|
config.SetNonString((JObject)config.GetObject("Serilog")!, "Serilog");
|
|
|
|
config.ConfigureLogging();
|
|
|
|
write(Serilog.Log.Logger);
|
|
Serilog.Log.CloseAndFlush();
|
|
|
|
return string.Join("\n", Directory.GetFiles(tempDir, "Log*.log").Select(File.ReadAllText));
|
|
}
|
|
|
|
/// <summary>Destructured, so the policy is what has to catch it: without one, Address would be written.</summary>
|
|
[TestMethod]
|
|
public void a_destructured_masked_type_is_reduced_to_its_masked_entry()
|
|
{
|
|
var written = LogThrough(logger => logger.Information("scanning {@Account}", new MaskedThing()));
|
|
|
|
StringAssert.Contains(written, Masked);
|
|
Assert.IsFalse(written.Contains(Secret, StringComparison.Ordinal), "the log contained the unmasked value");
|
|
}
|
|
|
|
/// <summary>The shape most of Libation's logging uses: an anonymous object holding the thing.</summary>
|
|
[TestMethod]
|
|
public void a_masked_type_nested_in_a_debug_object_is_reduced_too()
|
|
{
|
|
var written = LogThrough(logger => logger.Information("scanning {@DebugInfo}", new { Account = new MaskedThing(), Attempt = 2 }));
|
|
|
|
StringAssert.Contains(written, Masked);
|
|
Assert.IsFalse(written.Contains(Secret, StringComparison.Ordinal), "the log contained the unmasked value");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destructured on purpose. Without the transform a secret renders as an empty structure - safe, but it says
|
|
/// nothing, and the shape is the whole point of the redaction.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void a_destructured_secret_is_written_as_its_shape()
|
|
{
|
|
var written = LogThrough(logger => logger.Information("key {@Key}", new SecretString(Secret)));
|
|
|
|
StringAssert.Contains(written, $"[REDACTED length={Secret.Length}]");
|
|
Assert.IsFalse(written.Contains(Secret, StringComparison.Ordinal), "the log contained the secret");
|
|
}
|
|
}
|