feat(telemetry): add telemetry client and services for tracking events

- Implemented TelemetryClient to handle event queuing and flushing to the telemetry endpoint.
- Created TtfsTelemetryService for emitting specific telemetry events related to TTFS.
- Added tests for TelemetryClient to ensure event queuing and flushing functionality.
- Introduced models for reachability drift detection, including DriftResult and DriftedSink.
- Developed DriftApiService for interacting with the drift detection API.
- Updated FirstSignalCardComponent to emit telemetry events on signal appearance.
- Enhanced localization support for first signal component with i18n strings.
This commit is contained in:
master
2025-12-18 16:19:16 +02:00
parent 00d2c99af9
commit 811f35cba7
114 changed files with 13702 additions and 268 deletions

View File

@@ -0,0 +1,312 @@
// -----------------------------------------------------------------------------
// PolicyDecisionAttestationServiceTests.cs
// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
// Description: Unit tests for PolicyDecisionAttestationService.
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.Policy.Engine.Attestation;
using StellaOps.Policy.Engine.Vex;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Attestation;
public class PolicyDecisionAttestationServiceTests
{
private readonly Mock<IOptionsMonitor<PolicyDecisionAttestationOptions>> _optionsMock;
private readonly Mock<IVexSignerClient> _signerClientMock;
private readonly Mock<IVexRekorClient> _rekorClientMock;
private readonly PolicyDecisionAttestationService _service;
public PolicyDecisionAttestationServiceTests()
{
_optionsMock = new Mock<IOptionsMonitor<PolicyDecisionAttestationOptions>>();
_optionsMock.Setup(x => x.CurrentValue).Returns(new PolicyDecisionAttestationOptions
{
Enabled = true,
UseSignerService = true,
DefaultTtlHours = 24
});
_signerClientMock = new Mock<IVexSignerClient>();
_rekorClientMock = new Mock<IVexRekorClient>();
_service = new PolicyDecisionAttestationService(
_signerClientMock.Object,
_rekorClientMock.Object,
_optionsMock.Object,
TimeProvider.System,
NullLogger<PolicyDecisionAttestationService>.Instance);
}
[Fact]
public async Task CreateAttestationAsync_WhenDisabled_ReturnsFailure()
{
// Arrange
_optionsMock.Setup(x => x.CurrentValue).Returns(new PolicyDecisionAttestationOptions
{
Enabled = false
});
var request = CreateTestRequest();
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.False(result.Success);
Assert.Contains("disabled", result.Error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task CreateAttestationAsync_WithSignerClient_CallsSigner()
{
// Arrange
_signerClientMock.Setup(x => x.SignAsync(
It.IsAny<VexSignerRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResponse
{
Success = true,
AttestationDigest = "sha256:abc123",
KeyId = "key-1"
});
var request = CreateTestRequest();
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
Assert.Equal("sha256:abc123", result.AttestationDigest);
Assert.Equal("key-1", result.KeyId);
_signerClientMock.Verify(x => x.SignAsync(
It.Is<VexSignerRequest>(r => r.PayloadType == "stella.ops/policy-decision@v1"),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task CreateAttestationAsync_WhenSigningFails_ReturnsFailure()
{
// Arrange
_signerClientMock.Setup(x => x.SignAsync(
It.IsAny<VexSignerRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResponse
{
Success = false,
Error = "Key not found"
});
var request = CreateTestRequest();
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.False(result.Success);
Assert.Contains("Key not found", result.Error);
}
[Fact]
public async Task CreateAttestationAsync_WithRekorSubmission_SubmitsToRekor()
{
// Arrange
_signerClientMock.Setup(x => x.SignAsync(
It.IsAny<VexSignerRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResponse
{
Success = true,
AttestationDigest = "sha256:abc123",
KeyId = "key-1"
});
_rekorClientMock.Setup(x => x.SubmitAsync(
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexRekorResponse
{
Success = true,
LogIndex = 12345,
Uuid = "rekor-uuid-123"
});
var request = CreateTestRequest() with { SubmitToRekor = true };
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
Assert.NotNull(result.RekorResult);
Assert.True(result.RekorResult.Success);
Assert.Equal(12345, result.RekorResult.LogIndex);
_rekorClientMock.Verify(x => x.SubmitAsync(
"sha256:abc123",
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task CreateAttestationAsync_WithoutSignerClient_CreatesUnsignedAttestation()
{
// Arrange
var serviceWithoutSigner = new PolicyDecisionAttestationService(
signerClient: null,
rekorClient: null,
_optionsMock.Object,
TimeProvider.System,
NullLogger<PolicyDecisionAttestationService>.Instance);
var request = CreateTestRequest();
// Act
var result = await serviceWithoutSigner.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
Assert.StartsWith("sha256:", result.AttestationDigest);
Assert.Null(result.KeyId);
}
[Fact]
public async Task CreateAttestationAsync_IncludesAllSubjects()
{
// Arrange
_signerClientMock.Setup(x => x.SignAsync(
It.IsAny<VexSignerRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResponse
{
Success = true,
AttestationDigest = "sha256:abc123"
});
var request = CreateTestRequest() with
{
Subjects = new[]
{
new AttestationSubject
{
Name = "example.com/image:v1",
Digest = new Dictionary<string, string> { ["sha256"] = "abc123" }
},
new AttestationSubject
{
Name = "example.com/image:v2",
Digest = new Dictionary<string, string> { ["sha256"] = "def456" }
}
}
};
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
}
[Fact]
public async Task CreateAttestationAsync_SetsExpirationFromOptions()
{
// Arrange
_optionsMock.Setup(x => x.CurrentValue).Returns(new PolicyDecisionAttestationOptions
{
Enabled = true,
UseSignerService = false,
DefaultTtlHours = 48
});
var serviceWithOptions = new PolicyDecisionAttestationService(
signerClient: null,
rekorClient: null,
_optionsMock.Object,
TimeProvider.System,
NullLogger<PolicyDecisionAttestationService>.Instance);
var request = CreateTestRequest();
// Act
var result = await serviceWithOptions.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
}
[Fact]
public async Task SubmitToRekorAsync_WhenNoClient_ReturnsFailure()
{
// Arrange
var serviceWithoutRekor = new PolicyDecisionAttestationService(
_signerClientMock.Object,
rekorClient: null,
_optionsMock.Object,
TimeProvider.System,
NullLogger<PolicyDecisionAttestationService>.Instance);
// Act
var result = await serviceWithoutRekor.SubmitToRekorAsync("sha256:test");
// Assert
Assert.False(result.Success);
Assert.Contains("not available", result.Error);
}
[Fact]
public async Task VerifyAsync_ReturnsNotImplemented()
{
// Act
var result = await _service.VerifyAsync("sha256:test");
// Assert
Assert.False(result.Valid);
Assert.Contains("not yet implemented", result.Issues![0], StringComparison.OrdinalIgnoreCase);
}
private static PolicyDecisionAttestationRequest CreateTestRequest()
{
return new PolicyDecisionAttestationRequest
{
Predicate = new PolicyDecisionPredicate
{
Policy = new PolicyReference
{
Id = "test-policy",
Version = "1.0.0",
Name = "Test Policy"
},
Inputs = new PolicyDecisionInputs
{
Subjects = new[]
{
new SubjectReference
{
Name = "example.com/image:v1",
Digest = "sha256:abc123"
}
}
},
Result = new PolicyDecisionResult
{
Decision = PolicyDecision.Allow,
Summary = "All gates passed"
}
},
Subjects = new[]
{
new AttestationSubject
{
Name = "example.com/image:v1",
Digest = new Dictionary<string, string> { ["sha256"] = "abc123" }
}
}
};
}
}