172 lines
4.5 KiB
C#
172 lines
4.5 KiB
C#
using FluentAssertions;
|
|
using StellaOps.Scanner.Analyzers.Secrets;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Secrets.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class PayloadMaskerTests
|
|
{
|
|
private readonly PayloadMasker _masker = new();
|
|
|
|
[Fact]
|
|
public void Mask_EmptySpan_ReturnsEmpty()
|
|
{
|
|
_masker.Mask(ReadOnlySpan<char>.Empty).Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_ShortValue_ReturnsMaskChars()
|
|
{
|
|
// Values shorter than prefix+suffix get masked placeholder
|
|
var result = _masker.Mask("abc".AsSpan());
|
|
|
|
result.Should().Contain("*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_StandardValue_PreservesPrefixAndSuffix()
|
|
{
|
|
var result = _masker.Mask("1234567890".AsSpan());
|
|
|
|
// Default: 4 char prefix, 2 char suffix
|
|
result.Should().StartWith("1234");
|
|
result.Should().EndWith("90");
|
|
result.Should().Contain("****");
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_AwsAccessKey_PreservesPrefix()
|
|
{
|
|
var awsKey = "AKIAIOSFODNN7EXAMPLE";
|
|
|
|
var result = _masker.Mask(awsKey.AsSpan());
|
|
|
|
result.Should().StartWith("AKIA");
|
|
result.Should().EndWith("LE");
|
|
result.Should().Contain("****");
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_WithPrefixHint_UsesCustomPrefixLength()
|
|
{
|
|
var apiKey = "sk-proj-abcdefghijklmnop";
|
|
|
|
// MaxExposedChars is 6, so prefix:8 + suffix:2 gets scaled down
|
|
var result = _masker.Mask(apiKey.AsSpan(), "prefix:4,suffix:2");
|
|
|
|
result.Should().StartWith("sk-p");
|
|
result.Should().Contain("****");
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_LongValue_MasksMiddle()
|
|
{
|
|
var longSecret = "verylongsecretthatexceeds100characters" +
|
|
"andshouldbemaskkedproperlywithoutexpo" +
|
|
"singtheentirecontentstoanyoneviewingit";
|
|
|
|
var result = _masker.Mask(longSecret.AsSpan());
|
|
|
|
// Should contain mask characters and be shorter than original
|
|
result.Should().Contain("****");
|
|
result.Length.Should().BeLessThan(longSecret.Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_IsDeterministic()
|
|
{
|
|
var secret = "AKIAIOSFODNN7EXAMPLE";
|
|
|
|
var result1 = _masker.Mask(secret.AsSpan());
|
|
var result2 = _masker.Mask(secret.AsSpan());
|
|
|
|
result1.Should().Be(result2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_NeverExposesFullSecret()
|
|
{
|
|
var secret = "supersecretkey123";
|
|
|
|
var result = _masker.Mask(secret.AsSpan());
|
|
|
|
result.Should().NotBe(secret);
|
|
result.Should().Contain("*");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("prefix:6,suffix:0")]
|
|
[InlineData("prefix:0,suffix:6")]
|
|
[InlineData("prefix:3,suffix:3")]
|
|
public void Mask_WithVariousHints_RespectsTotalLimit(string hint)
|
|
{
|
|
var secret = "abcdefghijklmnopqrstuvwxyz";
|
|
|
|
var result = _masker.Mask(secret.AsSpan(), hint);
|
|
|
|
var visibleChars = result.Replace("*", "").Length;
|
|
visibleChars.Should().BeLessThanOrEqualTo(PayloadMasker.MaxExposedChars);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_EnforcesMinOutputLength()
|
|
{
|
|
var secret = "abcdefghijklmnop";
|
|
|
|
var result = _masker.Mask(secret.AsSpan());
|
|
|
|
result.Length.Should().BeGreaterThanOrEqualTo(PayloadMasker.MinOutputLength);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_ByteOverload_DecodesUtf8()
|
|
{
|
|
var text = "secretpassword123";
|
|
var bytes = System.Text.Encoding.UTF8.GetBytes(text);
|
|
|
|
var result = _masker.Mask(bytes.AsSpan());
|
|
|
|
result.Should().Contain("****");
|
|
result.Should().StartWith("secr");
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_EmptyByteSpan_ReturnsEmpty()
|
|
{
|
|
_masker.Mask(ReadOnlySpan<byte>.Empty).Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_InvalidHint_UsesDefaults()
|
|
{
|
|
var secret = "abcdefghijklmnop";
|
|
|
|
var result1 = _masker.Mask(secret.AsSpan(), "invalid:hint:format");
|
|
var result2 = _masker.Mask(secret.AsSpan());
|
|
|
|
result1.Should().Be(result2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_UsesCorrectMaskChar()
|
|
{
|
|
var secret = "abcdefghijklmnop";
|
|
|
|
var result = _masker.Mask(secret.AsSpan());
|
|
|
|
result.Should().Contain(PayloadMasker.MaskChar.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void Mask_MaskLengthLimited()
|
|
{
|
|
var longSecret = new string('x', 100);
|
|
|
|
var result = _masker.Mask(longSecret.AsSpan());
|
|
|
|
// Count mask characters
|
|
var maskCount = result.Count(c => c == PayloadMasker.MaskChar);
|
|
maskCount.Should().BeLessThanOrEqualTo(PayloadMasker.MaxMaskLength);
|
|
}
|
|
}
|