Add tests and implement StubBearer authentication for Signer endpoints
- 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.
This commit is contained in:
		
							
								
								
									
										198
									
								
								tools/NotifySmokeCheck/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								tools/NotifySmokeCheck/Program.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
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.");
 | 
			
		||||
		Reference in New Issue
	
	Block a user