Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			- 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.");
 |