old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
// <copyright file="VexStatementChangeEventTests.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_006_EXCITITOR_vex_change_events (EXC-VEX-004)
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Excititor.Core.Observations;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Excititor.Core.Tests.Observations;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class VexStatementChangeEventTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTimestamp = new(2026, 1, 15, 10, 30, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void CreateStatementAdded_GeneratesDeterministicEventId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var event1 = VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "not_affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
var event2 = VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "not_affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
// Assert - Same inputs should produce same event ID
|
||||
Assert.Equal(event1.EventId, event2.EventId);
|
||||
Assert.StartsWith("vex-evt-", event1.EventId);
|
||||
Assert.Equal(VexTimelineEventTypes.StatementAdded, event1.EventType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateStatementAdded_DifferentInputsProduceDifferentEventIds()
|
||||
{
|
||||
// Arrange & Act
|
||||
var event1 = VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "not_affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
var event2 = VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-5678", // Different CVE
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "not_affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0002:v1",
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
// Assert - Different inputs should produce different event IDs
|
||||
Assert.NotEqual(event1.EventId, event2.EventId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateStatementSuperseded_IncludesSupersedesReference()
|
||||
{
|
||||
// Arrange & Act
|
||||
var evt = VexStatementChangeEventFactory.CreateStatementSuperseded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "fixed",
|
||||
previousStatus: "not_affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v2",
|
||||
supersedes: ImmutableArray.Create("default:redhat:VEX-2026-0001:v1"),
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTimelineEventTypes.StatementSuperseded, evt.EventType);
|
||||
Assert.Equal("fixed", evt.NewStatus);
|
||||
Assert.Equal("not_affected", evt.PreviousStatus);
|
||||
Assert.Single(evt.Supersedes);
|
||||
Assert.Equal("default:redhat:VEX-2026-0001:v1", evt.Supersedes[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateConflictDetected_IncludesConflictDetails()
|
||||
{
|
||||
// Arrange
|
||||
var conflictingStatuses = ImmutableArray.Create(
|
||||
new VexConflictingStatus
|
||||
{
|
||||
ProviderId = "vendor:redhat",
|
||||
Status = "not_affected",
|
||||
Justification = "CODE_NOT_REACHABLE",
|
||||
TrustScore = 0.95
|
||||
},
|
||||
new VexConflictingStatus
|
||||
{
|
||||
ProviderId = "vendor:ubuntu",
|
||||
Status = "affected",
|
||||
Justification = null,
|
||||
TrustScore = 0.85
|
||||
});
|
||||
|
||||
// Act
|
||||
var evt = VexStatementChangeEventFactory.CreateConflictDetected(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
conflictType: "status_mismatch",
|
||||
conflictingStatuses: conflictingStatuses,
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTimelineEventTypes.StatementConflict, evt.EventType);
|
||||
Assert.NotNull(evt.ConflictDetails);
|
||||
Assert.Equal("status_mismatch", evt.ConflictDetails!.ConflictType);
|
||||
Assert.Equal(2, evt.ConflictDetails.ConflictingStatuses.Length);
|
||||
Assert.False(evt.ConflictDetails.AutoResolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConflictDetails_SortsStatusesByProviderId()
|
||||
{
|
||||
// Arrange - Providers in wrong order
|
||||
var conflictingStatuses = ImmutableArray.Create(
|
||||
new VexConflictingStatus
|
||||
{
|
||||
ProviderId = "vendor:ubuntu",
|
||||
Status = "affected",
|
||||
Justification = null,
|
||||
TrustScore = 0.85
|
||||
},
|
||||
new VexConflictingStatus
|
||||
{
|
||||
ProviderId = "vendor:redhat",
|
||||
Status = "not_affected",
|
||||
Justification = "CODE_NOT_REACHABLE",
|
||||
TrustScore = 0.95
|
||||
});
|
||||
|
||||
// Act
|
||||
var evt = VexStatementChangeEventFactory.CreateConflictDetected(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
conflictType: "status_mismatch",
|
||||
conflictingStatuses: conflictingStatuses,
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
// Assert - Should be sorted by provider ID for determinism
|
||||
Assert.Equal("vendor:redhat", evt.ConflictDetails!.ConflictingStatuses[0].ProviderId);
|
||||
Assert.Equal("vendor:ubuntu", evt.ConflictDetails.ConflictingStatuses[1].ProviderId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventId_IsIdempotentAcrossMultipleInvocations()
|
||||
{
|
||||
// Arrange
|
||||
var provenance = new VexStatementProvenance
|
||||
{
|
||||
DocumentHash = "sha256:abc123",
|
||||
DocumentUri = "https://vendor.example.com/vex/VEX-2026-0001.json",
|
||||
SourceTimestamp = FixedTimestamp.AddHours(-1),
|
||||
Author = "security@vendor.example.com",
|
||||
TrustScore = 0.95
|
||||
};
|
||||
|
||||
// Act - Create same event multiple times
|
||||
var events = new VexStatementChangeEvent[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
events[i] = VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "not_affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp,
|
||||
provenance: provenance);
|
||||
}
|
||||
|
||||
// Assert - All event IDs should be identical
|
||||
var firstEventId = events[0].EventId;
|
||||
foreach (var evt in events)
|
||||
{
|
||||
Assert.Equal(firstEventId, evt.EventId);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateStatusChanged_TracksStatusTransition()
|
||||
{
|
||||
// Arrange & Act
|
||||
var evt = VexStatementChangeEventFactory.CreateStatusChanged(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
newStatus: "fixed",
|
||||
previousStatus: "affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v3",
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTimelineEventTypes.StatusChanged, evt.EventType);
|
||||
Assert.Equal("fixed", evt.NewStatus);
|
||||
Assert.Equal("affected", evt.PreviousStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventOrdering_DeterministicByTimestampThenProvider()
|
||||
{
|
||||
// Arrange - Create events with same timestamp but different providers
|
||||
var events = new[]
|
||||
{
|
||||
VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "not_affected",
|
||||
providerId: "vendor:ubuntu",
|
||||
observationId: "default:ubuntu:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp),
|
||||
VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp),
|
||||
VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "under_investigation",
|
||||
providerId: "vendor:debian",
|
||||
observationId: "default:debian:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp),
|
||||
};
|
||||
|
||||
// Act - Sort by (timestamp, providerId) for deterministic ordering
|
||||
var sorted = events
|
||||
.OrderBy(e => e.OccurredAtUtc)
|
||||
.ThenBy(e => e.ProviderId)
|
||||
.ToArray();
|
||||
|
||||
// Assert - Should be sorted by provider ID alphabetically
|
||||
Assert.Equal("vendor:debian", sorted[0].ProviderId);
|
||||
Assert.Equal("vendor:redhat", sorted[1].ProviderId);
|
||||
Assert.Equal("vendor:ubuntu", sorted[2].ProviderId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Provenance_PreservedInEvent()
|
||||
{
|
||||
// Arrange
|
||||
var provenance = new VexStatementProvenance
|
||||
{
|
||||
DocumentHash = "sha256:abc123def456",
|
||||
DocumentUri = "https://vendor.example.com/vex/VEX-2026-0001.json",
|
||||
SourceTimestamp = new DateTimeOffset(2026, 1, 15, 9, 0, 0, TimeSpan.Zero),
|
||||
Author = "security@vendor.example.com",
|
||||
TrustScore = 0.95
|
||||
};
|
||||
|
||||
// Act
|
||||
var evt = VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: "default",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "not_affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp,
|
||||
provenance: provenance);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(evt.Provenance);
|
||||
Assert.Equal("sha256:abc123def456", evt.Provenance!.DocumentHash);
|
||||
Assert.Equal("https://vendor.example.com/vex/VEX-2026-0001.json", evt.Provenance.DocumentUri);
|
||||
Assert.Equal(0.95, evt.Provenance.TrustScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TenantNormalization_LowerCasesAndTrims()
|
||||
{
|
||||
// Arrange & Act
|
||||
var evt = VexStatementChangeEventFactory.CreateStatementAdded(
|
||||
tenant: " DEFAULT ",
|
||||
vulnerabilityId: "CVE-2026-1234",
|
||||
productKey: "pkg:npm/lodash@4.17.21",
|
||||
status: "not_affected",
|
||||
providerId: "vendor:redhat",
|
||||
observationId: "default:redhat:VEX-2026-0001:v1",
|
||||
occurredAtUtc: FixedTimestamp);
|
||||
|
||||
// Assert - Tenant should be normalized
|
||||
Assert.Equal("default", evt.Tenant);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user