Add tests and implement StubBearer authentication for Signer endpoints
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			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.
This commit is contained in:
		| @@ -0,0 +1,302 @@ | ||||
| using System.Globalization; | ||||
| using StellaOps.Bench.ScannerAnalyzers.Scenarios; | ||||
|  | ||||
| namespace StellaOps.Bench.ScannerAnalyzers; | ||||
|  | ||||
| internal static class Program | ||||
| { | ||||
|     public static async Task<int> Main(string[] args) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             var options = ProgramOptions.Parse(args); | ||||
|             var config = await BenchmarkConfig.LoadAsync(options.ConfigPath).ConfigureAwait(false); | ||||
|  | ||||
|             var iterations = options.Iterations ?? config.Iterations ?? 5; | ||||
|             var thresholdMs = options.ThresholdMs ?? config.ThresholdMs ?? 5000; | ||||
|             var repoRoot = ResolveRepoRoot(options.RepoRoot, options.ConfigPath); | ||||
|  | ||||
|             var results = new List<ScenarioResult>(); | ||||
|             var failures = new List<string>(); | ||||
|  | ||||
|             foreach (var scenario in config.Scenarios) | ||||
|             { | ||||
|                 var runner = ScenarioRunnerFactory.Create(scenario); | ||||
|                 var scenarioRoot = ResolveScenarioRoot(repoRoot, scenario.Root!); | ||||
|  | ||||
|                 var execution = await runner.ExecuteAsync(scenarioRoot, iterations, CancellationToken.None).ConfigureAwait(false); | ||||
|                 var stats = ScenarioStatistics.FromDurations(execution.Durations); | ||||
|                 var scenarioThreshold = scenario.ThresholdMs ?? thresholdMs; | ||||
|  | ||||
|                 results.Add(new ScenarioResult( | ||||
|                     scenario.Id!, | ||||
|                     scenario.Label ?? scenario.Id!, | ||||
|                     execution.SampleCount, | ||||
|                     stats.MeanMs, | ||||
|                     stats.P95Ms, | ||||
|                     stats.MaxMs, | ||||
|                     iterations)); | ||||
|  | ||||
|                 if (stats.MaxMs > scenarioThreshold) | ||||
|                 { | ||||
|                     failures.Add($"{scenario.Id} exceeded threshold: {stats.MaxMs:F2} ms > {scenarioThreshold:F2} ms"); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             TablePrinter.Print(results); | ||||
|  | ||||
|             if (!string.IsNullOrWhiteSpace(options.OutPath)) | ||||
|             { | ||||
|                 CsvWriter.Write(options.OutPath!, results); | ||||
|             } | ||||
|  | ||||
|             if (failures.Count > 0) | ||||
|             { | ||||
|                 Console.Error.WriteLine(); | ||||
|                 Console.Error.WriteLine("Performance threshold exceeded:"); | ||||
|                 foreach (var failure in failures) | ||||
|                 { | ||||
|                     Console.Error.WriteLine($" - {failure}"); | ||||
|                 } | ||||
|  | ||||
|                 return 1; | ||||
|             } | ||||
|  | ||||
|             return 0; | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Console.Error.WriteLine(ex.Message); | ||||
|             return 1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static string ResolveRepoRoot(string? overridePath, string configPath) | ||||
|     { | ||||
|         if (!string.IsNullOrWhiteSpace(overridePath)) | ||||
|         { | ||||
|             return Path.GetFullPath(overridePath); | ||||
|         } | ||||
|  | ||||
|         var configDirectory = Path.GetDirectoryName(configPath); | ||||
|         if (string.IsNullOrWhiteSpace(configDirectory)) | ||||
|         { | ||||
|             return Directory.GetCurrentDirectory(); | ||||
|         } | ||||
|  | ||||
|         return Path.GetFullPath(Path.Combine(configDirectory, "..", "..")); | ||||
|     } | ||||
|  | ||||
|     private static string ResolveScenarioRoot(string repoRoot, string relativeRoot) | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(relativeRoot)) | ||||
|         { | ||||
|             throw new InvalidOperationException("Scenario root is required."); | ||||
|         } | ||||
|  | ||||
|         var combined = Path.GetFullPath(Path.Combine(repoRoot, relativeRoot)); | ||||
|         if (!PathUtilities.IsWithinRoot(repoRoot, combined)) | ||||
|         { | ||||
|             throw new InvalidOperationException($"Scenario root '{relativeRoot}' escapes repository root '{repoRoot}'."); | ||||
|         } | ||||
|  | ||||
|         if (!Directory.Exists(combined)) | ||||
|         { | ||||
|             throw new DirectoryNotFoundException($"Scenario root '{combined}' does not exist."); | ||||
|         } | ||||
|  | ||||
|         return combined; | ||||
|     } | ||||
|  | ||||
|     private sealed record ProgramOptions(string ConfigPath, int? Iterations, double? ThresholdMs, string? OutPath, string? RepoRoot) | ||||
|     { | ||||
|         public static ProgramOptions Parse(string[] args) | ||||
|         { | ||||
|             var configPath = DefaultConfigPath(); | ||||
|             int? iterations = null; | ||||
|             double? thresholdMs = null; | ||||
|             string? outPath = null; | ||||
|             string? repoRoot = null; | ||||
|  | ||||
|             for (var index = 0; index < args.Length; index++) | ||||
|             { | ||||
|                 var current = args[index]; | ||||
|                 switch (current) | ||||
|                 { | ||||
|                     case "--config": | ||||
|                         EnsureNext(args, index); | ||||
|                         configPath = Path.GetFullPath(args[++index]); | ||||
|                         break; | ||||
|                     case "--iterations": | ||||
|                         EnsureNext(args, index); | ||||
|                         iterations = int.Parse(args[++index], CultureInfo.InvariantCulture); | ||||
|                         break; | ||||
|                     case "--threshold-ms": | ||||
|                         EnsureNext(args, index); | ||||
|                         thresholdMs = double.Parse(args[++index], CultureInfo.InvariantCulture); | ||||
|                         break; | ||||
|                     case "--out": | ||||
|                         EnsureNext(args, index); | ||||
|                         outPath = args[++index]; | ||||
|                         break; | ||||
|                     case "--repo-root": | ||||
|                     case "--samples": | ||||
|                         EnsureNext(args, index); | ||||
|                         repoRoot = args[++index]; | ||||
|                         break; | ||||
|                     default: | ||||
|                         throw new ArgumentException($"Unknown argument: {current}", nameof(args)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return new ProgramOptions(configPath, iterations, thresholdMs, outPath, repoRoot); | ||||
|         } | ||||
|  | ||||
|         private static string DefaultConfigPath() | ||||
|         { | ||||
|             var binaryDir = AppContext.BaseDirectory; | ||||
|             var projectRoot = Path.GetFullPath(Path.Combine(binaryDir, "..", "..", "..")); | ||||
|             var configDirectory = Path.GetFullPath(Path.Combine(projectRoot, "..")); | ||||
|             return Path.Combine(configDirectory, "config.json"); | ||||
|         } | ||||
|  | ||||
|         private static void EnsureNext(string[] args, int index) | ||||
|         { | ||||
|             if (index + 1 >= args.Length) | ||||
|             { | ||||
|                 throw new ArgumentException("Missing value for argument.", nameof(args)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private sealed record ScenarioResult( | ||||
|         string Id, | ||||
|         string Label, | ||||
|         int SampleCount, | ||||
|         double MeanMs, | ||||
|         double P95Ms, | ||||
|         double MaxMs, | ||||
|         int Iterations); | ||||
|  | ||||
|     private sealed record ScenarioStatistics(double MeanMs, double P95Ms, double MaxMs) | ||||
|     { | ||||
|         public static ScenarioStatistics FromDurations(IReadOnlyList<double> durations) | ||||
|         { | ||||
|             if (durations.Count == 0) | ||||
|             { | ||||
|                 return new ScenarioStatistics(0, 0, 0); | ||||
|             } | ||||
|  | ||||
|             var sorted = durations.ToArray(); | ||||
|             Array.Sort(sorted); | ||||
|  | ||||
|             var total = 0d; | ||||
|             foreach (var value in durations) | ||||
|             { | ||||
|                 total += value; | ||||
|             } | ||||
|  | ||||
|             var mean = total / durations.Count; | ||||
|             var p95 = Percentile(sorted, 95); | ||||
|             var max = sorted[^1]; | ||||
|  | ||||
|             return new ScenarioStatistics(mean, p95, max); | ||||
|         } | ||||
|  | ||||
|         private static double Percentile(IReadOnlyList<double> sorted, double percentile) | ||||
|         { | ||||
|             if (sorted.Count == 0) | ||||
|             { | ||||
|                 return 0; | ||||
|             } | ||||
|  | ||||
|             var rank = (percentile / 100d) * (sorted.Count - 1); | ||||
|             var lower = (int)Math.Floor(rank); | ||||
|             var upper = (int)Math.Ceiling(rank); | ||||
|             var weight = rank - lower; | ||||
|  | ||||
|             if (upper >= sorted.Count) | ||||
|             { | ||||
|                 return sorted[lower]; | ||||
|             } | ||||
|  | ||||
|             return sorted[lower] + weight * (sorted[upper] - sorted[lower]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class TablePrinter | ||||
|     { | ||||
|         public static void Print(IEnumerable<ScenarioResult> results) | ||||
|         { | ||||
|             Console.WriteLine("Scenario                     | Count |   Mean(ms) |    P95(ms) |     Max(ms)"); | ||||
|             Console.WriteLine("---------------------------- | ----- | --------- | --------- | ----------"); | ||||
|             foreach (var row in results) | ||||
|             { | ||||
|                 Console.WriteLine(FormatRow(row)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static string FormatRow(ScenarioResult row) | ||||
|         { | ||||
|             var idColumn = row.Id.Length <= 28 | ||||
|                 ? row.Id.PadRight(28) | ||||
|                 : row.Id[..28]; | ||||
|  | ||||
|             return string.Join(" | ", new[] | ||||
|             { | ||||
|                 idColumn, | ||||
|                 row.SampleCount.ToString(CultureInfo.InvariantCulture).PadLeft(5), | ||||
|                 row.MeanMs.ToString("F2", CultureInfo.InvariantCulture).PadLeft(9), | ||||
|                 row.P95Ms.ToString("F2", CultureInfo.InvariantCulture).PadLeft(9), | ||||
|                 row.MaxMs.ToString("F2", CultureInfo.InvariantCulture).PadLeft(10), | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class CsvWriter | ||||
|     { | ||||
|         public static void Write(string path, IEnumerable<ScenarioResult> results) | ||||
|         { | ||||
|             var resolvedPath = Path.GetFullPath(path); | ||||
|             var directory = Path.GetDirectoryName(resolvedPath); | ||||
|             if (!string.IsNullOrEmpty(directory)) | ||||
|             { | ||||
|                 Directory.CreateDirectory(directory); | ||||
|             } | ||||
|  | ||||
|             using var stream = new FileStream(resolvedPath, FileMode.Create, FileAccess.Write, FileShare.None); | ||||
|             using var writer = new StreamWriter(stream); | ||||
|             writer.WriteLine("scenario,iterations,sample_count,mean_ms,p95_ms,max_ms"); | ||||
|  | ||||
|             foreach (var row in results) | ||||
|             { | ||||
|                 writer.Write(row.Id); | ||||
|                 writer.Write(','); | ||||
|                 writer.Write(row.Iterations.ToString(CultureInfo.InvariantCulture)); | ||||
|                 writer.Write(','); | ||||
|                 writer.Write(row.SampleCount.ToString(CultureInfo.InvariantCulture)); | ||||
|                 writer.Write(','); | ||||
|                 writer.Write(row.MeanMs.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|                 writer.Write(','); | ||||
|                 writer.Write(row.P95Ms.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|                 writer.Write(','); | ||||
|                 writer.Write(row.MaxMs.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|                 writer.WriteLine(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     internal static class PathUtilities | ||||
|     { | ||||
|         public static bool IsWithinRoot(string root, string candidate) | ||||
|         { | ||||
|             var relative = Path.GetRelativePath(root, candidate); | ||||
|             if (string.IsNullOrEmpty(relative) || relative == ".") | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             return !relative.StartsWith("..", StringComparison.Ordinal) && !Path.IsPathRooted(relative); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user