- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints. - Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication. - Developed ConcelierExporterClient for managing Trivy DB settings and export operations. - Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering. - Implemented styles and HTML structure for Trivy DB settings page. - Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
		
			
				
	
	
		
			199 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			199 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using System.Globalization;
 | 
						||
using System.Net.Http.Headers;
 | 
						||
using System.Linq;
 | 
						||
using System.Text.Json;
 | 
						||
using StackExchange.Redis;
 | 
						||
 | 
						||
static string RequireEnv(string name)
 | 
						||
{
 | 
						||
    var value = Environment.GetEnvironmentVariable(name);
 | 
						||
    if (string.IsNullOrWhiteSpace(value))
 | 
						||
    {
 | 
						||
        throw new InvalidOperationException($"Environment variable '{name}' is required for Notify smoke validation.");
 | 
						||
    }
 | 
						||
 | 
						||
    return value;
 | 
						||
}
 | 
						||
 | 
						||
static string? GetField(StreamEntry entry, string fieldName)
 | 
						||
{
 | 
						||
    foreach (var pair in entry.Values)
 | 
						||
    {
 | 
						||
        if (string.Equals(pair.Name, fieldName, StringComparison.OrdinalIgnoreCase))
 | 
						||
        {
 | 
						||
            return pair.Value.ToString();
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    return null;
 | 
						||
}
 | 
						||
 | 
						||
static void Ensure(bool condition, string message)
 | 
						||
{
 | 
						||
    if (!condition)
 | 
						||
    {
 | 
						||
        throw new InvalidOperationException(message);
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
var redisDsn = RequireEnv("NOTIFY_SMOKE_REDIS_DSN");
 | 
						||
var redisStream = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_STREAM");
 | 
						||
if (string.IsNullOrWhiteSpace(redisStream))
 | 
						||
{
 | 
						||
    redisStream = "stella.events";
 | 
						||
}
 | 
						||
 | 
						||
var expectedKindsEnv = RequireEnv("NOTIFY_SMOKE_EXPECT_KINDS");
 | 
						||
 | 
						||
var expectedKinds = expectedKindsEnv
 | 
						||
    .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 | 
						||
    .Select(kind => kind.ToLowerInvariant())
 | 
						||
    .Distinct()
 | 
						||
    .ToArray();
 | 
						||
Ensure(expectedKinds.Length > 0, "Expected at least one event kind in NOTIFY_SMOKE_EXPECT_KINDS.");
 | 
						||
 | 
						||
var lookbackMinutesEnv = RequireEnv("NOTIFY_SMOKE_LOOKBACK_MINUTES");
 | 
						||
if (!double.TryParse(lookbackMinutesEnv, NumberStyles.Any, CultureInfo.InvariantCulture, out var lookbackMinutes))
 | 
						||
{
 | 
						||
    throw new InvalidOperationException("NOTIFY_SMOKE_LOOKBACK_MINUTES must be numeric.");
 | 
						||
}
 | 
						||
Ensure(lookbackMinutes > 0, "NOTIFY_SMOKE_LOOKBACK_MINUTES must be greater than zero.");
 | 
						||
 | 
						||
var now = DateTimeOffset.UtcNow;
 | 
						||
var sinceThreshold = now - TimeSpan.FromMinutes(Math.Max(1, lookbackMinutes));
 | 
						||
 | 
						||
Console.WriteLine($"ℹ️  Checking Redis stream '{redisStream}' for kinds [{string.Join(", ", expectedKinds)}] within the last {lookbackMinutes:F1} minutes.");
 | 
						||
 | 
						||
var redisConfig = ConfigurationOptions.Parse(redisDsn);
 | 
						||
redisConfig.AbortOnConnectFail = false;
 | 
						||
 | 
						||
await using var redisConnection = await ConnectionMultiplexer.ConnectAsync(redisConfig);
 | 
						||
var database = redisConnection.GetDatabase();
 | 
						||
 | 
						||
var streamEntries = await database.StreamRangeAsync(redisStream, "-", "+", count: 200);
 | 
						||
if (streamEntries.Length > 1)
 | 
						||
{
 | 
						||
    Array.Reverse(streamEntries);
 | 
						||
}
 | 
						||
Ensure(streamEntries.Length > 0, $"Redis stream '{redisStream}' is empty.");
 | 
						||
 | 
						||
var recentEntries = new List<StreamEntry>();
 | 
						||
foreach (var entry in streamEntries)
 | 
						||
{
 | 
						||
    var timestampText = GetField(entry, "ts");
 | 
						||
    if (timestampText is null)
 | 
						||
    {
 | 
						||
        continue;
 | 
						||
    }
 | 
						||
 | 
						||
    if (!DateTimeOffset.TryParse(timestampText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var entryTimestamp))
 | 
						||
    {
 | 
						||
        continue;
 | 
						||
    }
 | 
						||
 | 
						||
    if (entryTimestamp >= sinceThreshold)
 | 
						||
    {
 | 
						||
        recentEntries.Add(entry);
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
Ensure(recentEntries.Count > 0, $"No Redis events newer than {sinceThreshold:u} located in stream '{redisStream}'.");
 | 
						||
 | 
						||
var missingKinds = new List<string>();
 | 
						||
foreach (var kind in expectedKinds)
 | 
						||
{
 | 
						||
    var match = recentEntries.FirstOrDefault(entry =>
 | 
						||
    {
 | 
						||
        var entryKind = GetField(entry, "kind")?.ToLowerInvariant();
 | 
						||
        return entryKind == kind;
 | 
						||
    });
 | 
						||
 | 
						||
    if (match.Equals(default(StreamEntry)))
 | 
						||
    {
 | 
						||
        missingKinds.Add(kind);
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
Ensure(missingKinds.Count == 0, $"Missing expected Redis events for kinds: {string.Join(", ", missingKinds)}");
 | 
						||
 | 
						||
Console.WriteLine("✅ Redis event stream contains the expected scanner events.");
 | 
						||
 | 
						||
var notifyBaseUrl = RequireEnv("NOTIFY_SMOKE_NOTIFY_BASEURL").TrimEnd('/');
 | 
						||
var notifyToken = RequireEnv("NOTIFY_SMOKE_NOTIFY_TOKEN");
 | 
						||
var notifyTenant = RequireEnv("NOTIFY_SMOKE_NOTIFY_TENANT");
 | 
						||
var notifyTenantHeader = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_NOTIFY_TENANT_HEADER");
 | 
						||
if (string.IsNullOrWhiteSpace(notifyTenantHeader))
 | 
						||
{
 | 
						||
    notifyTenantHeader = "X-StellaOps-Tenant";
 | 
						||
}
 | 
						||
 | 
						||
var notifyTimeoutSeconds = 30;
 | 
						||
var notifyTimeoutEnv = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_NOTIFY_TIMEOUT_SECONDS");
 | 
						||
if (!string.IsNullOrWhiteSpace(notifyTimeoutEnv) && int.TryParse(notifyTimeoutEnv, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedTimeout))
 | 
						||
{
 | 
						||
    notifyTimeoutSeconds = Math.Max(5, parsedTimeout);
 | 
						||
}
 | 
						||
 | 
						||
using var httpClient = new HttpClient
 | 
						||
{
 | 
						||
    Timeout = TimeSpan.FromSeconds(notifyTimeoutSeconds),
 | 
						||
};
 | 
						||
 | 
						||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", notifyToken);
 | 
						||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 | 
						||
httpClient.DefaultRequestHeaders.Add(notifyTenantHeader, notifyTenant);
 | 
						||
 | 
						||
var sinceQuery = Uri.EscapeDataString(sinceThreshold.ToString("O", CultureInfo.InvariantCulture));
 | 
						||
var deliveriesUrl = $"{notifyBaseUrl}/api/v1/deliveries?since={sinceQuery}&limit=200";
 | 
						||
 | 
						||
Console.WriteLine($"ℹ️  Querying Notify deliveries via {deliveriesUrl}.");
 | 
						||
 | 
						||
using var response = await httpClient.GetAsync(deliveriesUrl);
 | 
						||
if (!response.IsSuccessStatusCode)
 | 
						||
{
 | 
						||
    var body = await response.Content.ReadAsStringAsync();
 | 
						||
    throw new InvalidOperationException($"Notify deliveries request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {body}");
 | 
						||
}
 | 
						||
 | 
						||
var json = await response.Content.ReadAsStringAsync();
 | 
						||
if (string.IsNullOrWhiteSpace(json))
 | 
						||
{
 | 
						||
    throw new InvalidOperationException("Notify deliveries response body was empty.");
 | 
						||
}
 | 
						||
 | 
						||
using var document = JsonDocument.Parse(json);
 | 
						||
var root = document.RootElement;
 | 
						||
 | 
						||
IEnumerable<JsonElement> EnumerateDeliveries(JsonElement element)
 | 
						||
{
 | 
						||
    return element.ValueKind switch
 | 
						||
    {
 | 
						||
        JsonValueKind.Array => element.EnumerateArray(),
 | 
						||
        JsonValueKind.Object when element.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array => items.EnumerateArray(),
 | 
						||
        _ => throw new InvalidOperationException("Notify deliveries response was not an array or did not contain an 'items' collection.")
 | 
						||
    };
 | 
						||
}
 | 
						||
 | 
						||
var deliveries = EnumerateDeliveries(root).ToArray();
 | 
						||
Ensure(deliveries.Length > 0, "Notify deliveries response did not return any records.");
 | 
						||
 | 
						||
var missingDeliveryKinds = new List<string>();
 | 
						||
foreach (var kind in expectedKinds)
 | 
						||
{
 | 
						||
    var found = deliveries.Any(delivery =>
 | 
						||
        delivery.TryGetProperty("kind", out var kindProperty) &&
 | 
						||
        kindProperty.GetString()?.Equals(kind, StringComparison.OrdinalIgnoreCase) == true &&
 | 
						||
        delivery.TryGetProperty("status", out var statusProperty) &&
 | 
						||
        !string.Equals(statusProperty.GetString(), "failed", StringComparison.OrdinalIgnoreCase));
 | 
						||
 | 
						||
    if (!found)
 | 
						||
    {
 | 
						||
        missingDeliveryKinds.Add(kind);
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
Ensure(missingDeliveryKinds.Count == 0, $"Notify deliveries missing successful records for kinds: {string.Join(", ", missingDeliveryKinds)}");
 | 
						||
 | 
						||
Console.WriteLine("✅ Notify deliveries include the expected scanner events.");
 | 
						||
Console.WriteLine("🎉 Notify smoke validation completed successfully.");
 |