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,279 @@ | ||||
| using System.Diagnostics; | ||||
| using System.Text; | ||||
| using System.Linq; | ||||
| using System.Text.Json; | ||||
| using System.Text.RegularExpressions; | ||||
| using StellaOps.Scanner.Analyzers.Lang; | ||||
| using StellaOps.Scanner.Analyzers.Lang.Java; | ||||
| using StellaOps.Scanner.Analyzers.Lang.Node; | ||||
|  | ||||
| namespace StellaOps.Bench.ScannerAnalyzers.Scenarios; | ||||
|  | ||||
| internal interface IScenarioRunner | ||||
| { | ||||
|     Task<ScenarioExecutionResult> ExecuteAsync(string rootPath, int iterations, CancellationToken cancellationToken); | ||||
| } | ||||
|  | ||||
| internal sealed record ScenarioExecutionResult(double[] Durations, int SampleCount); | ||||
|  | ||||
| internal static class ScenarioRunnerFactory | ||||
| { | ||||
|     public static IScenarioRunner Create(BenchmarkScenarioConfig scenario) | ||||
|     { | ||||
|         if (scenario.HasAnalyzers) | ||||
|         { | ||||
|             return new LanguageAnalyzerScenarioRunner(scenario.Analyzers!); | ||||
|         } | ||||
|  | ||||
|         if (string.IsNullOrWhiteSpace(scenario.Parser) || string.IsNullOrWhiteSpace(scenario.Matcher)) | ||||
|         { | ||||
|             throw new InvalidOperationException($"Scenario '{scenario.Id}' missing parser or matcher configuration."); | ||||
|         } | ||||
|  | ||||
|         return new MetadataWalkScenarioRunner(scenario.Parser, scenario.Matcher); | ||||
|     } | ||||
| } | ||||
|  | ||||
| internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner | ||||
| { | ||||
|     private readonly IReadOnlyList<Func<ILanguageAnalyzer>> _analyzerFactories; | ||||
|  | ||||
|     public LanguageAnalyzerScenarioRunner(IEnumerable<string> analyzerIds) | ||||
|     { | ||||
|         if (analyzerIds is null) | ||||
|         { | ||||
|             throw new ArgumentNullException(nameof(analyzerIds)); | ||||
|         } | ||||
|  | ||||
|         _analyzerFactories = analyzerIds | ||||
|             .Where(static id => !string.IsNullOrWhiteSpace(id)) | ||||
|             .Select(CreateFactory) | ||||
|             .ToArray(); | ||||
|  | ||||
|         if (_analyzerFactories.Count == 0) | ||||
|         { | ||||
|             throw new InvalidOperationException("At least one analyzer id must be provided."); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<ScenarioExecutionResult> ExecuteAsync(string rootPath, int iterations, CancellationToken cancellationToken) | ||||
|     { | ||||
|         if (iterations <= 0) | ||||
|         { | ||||
|             throw new ArgumentOutOfRangeException(nameof(iterations), iterations, "Iterations must be positive."); | ||||
|         } | ||||
|  | ||||
|         var analyzers = _analyzerFactories.Select(factory => factory()).ToArray(); | ||||
|         var engine = new LanguageAnalyzerEngine(analyzers); | ||||
|         var durations = new double[iterations]; | ||||
|         var componentCount = -1; | ||||
|  | ||||
|         for (var i = 0; i < iterations; i++) | ||||
|         { | ||||
|             cancellationToken.ThrowIfCancellationRequested(); | ||||
|  | ||||
|             var context = new LanguageAnalyzerContext(rootPath, TimeProvider.System); | ||||
|             var stopwatch = Stopwatch.StartNew(); | ||||
|             var result = await engine.AnalyzeAsync(context, cancellationToken).ConfigureAwait(false); | ||||
|             stopwatch.Stop(); | ||||
|  | ||||
|             durations[i] = stopwatch.Elapsed.TotalMilliseconds; | ||||
|  | ||||
|             var currentCount = result.Components.Count; | ||||
|             if (componentCount < 0) | ||||
|             { | ||||
|                 componentCount = currentCount; | ||||
|             } | ||||
|             else if (componentCount != currentCount) | ||||
|             { | ||||
|                 throw new InvalidOperationException($"Analyzer output count changed between iterations ({componentCount} vs {currentCount})."); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (componentCount < 0) | ||||
|         { | ||||
|             componentCount = 0; | ||||
|         } | ||||
|  | ||||
|         return new ScenarioExecutionResult(durations, componentCount); | ||||
|     } | ||||
|  | ||||
|     private static Func<ILanguageAnalyzer> CreateFactory(string analyzerId) | ||||
|     { | ||||
|         var id = analyzerId.Trim().ToLowerInvariant(); | ||||
|         return id switch | ||||
|         { | ||||
|             "java" => static () => new JavaLanguageAnalyzer(), | ||||
|             "node" => static () => new NodeLanguageAnalyzer(), | ||||
|             _ => throw new InvalidOperationException($"Unsupported analyzer '{analyzerId}'."), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| internal sealed class MetadataWalkScenarioRunner : IScenarioRunner | ||||
| { | ||||
|     private readonly Regex _matcher; | ||||
|     private readonly string _parserKind; | ||||
|  | ||||
|     public MetadataWalkScenarioRunner(string parserKind, string globPattern) | ||||
|     { | ||||
|         _parserKind = parserKind?.Trim().ToLowerInvariant() ?? throw new ArgumentNullException(nameof(parserKind)); | ||||
|         _matcher = GlobToRegex(globPattern ?? throw new ArgumentNullException(nameof(globPattern))); | ||||
|     } | ||||
|  | ||||
|     public async Task<ScenarioExecutionResult> ExecuteAsync(string rootPath, int iterations, CancellationToken cancellationToken) | ||||
|     { | ||||
|         if (iterations <= 0) | ||||
|         { | ||||
|             throw new ArgumentOutOfRangeException(nameof(iterations), iterations, "Iterations must be positive."); | ||||
|         } | ||||
|  | ||||
|         var durations = new double[iterations]; | ||||
|         var sampleCount = -1; | ||||
|  | ||||
|         for (var i = 0; i < iterations; i++) | ||||
|         { | ||||
|             cancellationToken.ThrowIfCancellationRequested(); | ||||
|  | ||||
|             var stopwatch = Stopwatch.StartNew(); | ||||
|             var files = EnumerateMatchingFiles(rootPath); | ||||
|             if (files.Count == 0) | ||||
|             { | ||||
|                 throw new InvalidOperationException($"Parser '{_parserKind}' matched zero files under '{rootPath}'."); | ||||
|             } | ||||
|  | ||||
|             foreach (var file in files) | ||||
|             { | ||||
|                 cancellationToken.ThrowIfCancellationRequested(); | ||||
|                 await ParseAsync(file).ConfigureAwait(false); | ||||
|             } | ||||
|  | ||||
|             stopwatch.Stop(); | ||||
|             durations[i] = stopwatch.Elapsed.TotalMilliseconds; | ||||
|  | ||||
|             if (sampleCount < 0) | ||||
|             { | ||||
|                 sampleCount = files.Count; | ||||
|             } | ||||
|             else if (sampleCount != files.Count) | ||||
|             { | ||||
|                 throw new InvalidOperationException($"File count changed between iterations ({sampleCount} vs {files.Count})."); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (sampleCount < 0) | ||||
|         { | ||||
|             sampleCount = 0; | ||||
|         } | ||||
|  | ||||
|         return new ScenarioExecutionResult(durations, sampleCount); | ||||
|     } | ||||
|  | ||||
|     private async ValueTask ParseAsync(string filePath) | ||||
|     { | ||||
|         switch (_parserKind) | ||||
|         { | ||||
|             case "node": | ||||
|                 { | ||||
|                     using var stream = File.OpenRead(filePath); | ||||
|                     using var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false); | ||||
|  | ||||
|                     if (!document.RootElement.TryGetProperty("name", out var name) || name.ValueKind != JsonValueKind.String) | ||||
|                     { | ||||
|                         throw new InvalidOperationException($"package.json '{filePath}' missing name."); | ||||
|                     } | ||||
|  | ||||
|                     if (!document.RootElement.TryGetProperty("version", out var version) || version.ValueKind != JsonValueKind.String) | ||||
|                     { | ||||
|                         throw new InvalidOperationException($"package.json '{filePath}' missing version."); | ||||
|                     } | ||||
|                 } | ||||
|                 break; | ||||
|             case "python": | ||||
|                 { | ||||
|                     var (name, version) = await ParsePythonMetadataAsync(filePath).ConfigureAwait(false); | ||||
|                     if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) | ||||
|                     { | ||||
|                         throw new InvalidOperationException($"METADATA '{filePath}' missing Name/Version."); | ||||
|                     } | ||||
|                 } | ||||
|                 break; | ||||
|             default: | ||||
|                 throw new InvalidOperationException($"Unknown parser '{_parserKind}'."); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static async Task<(string? Name, string? Version)> ParsePythonMetadataAsync(string filePath) | ||||
|     { | ||||
|         using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); | ||||
|         using var reader = new StreamReader(stream); | ||||
|  | ||||
|         string? name = null; | ||||
|         string? version = null; | ||||
|  | ||||
|         while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line) | ||||
|         { | ||||
|             if (line.StartsWith("Name:", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 name ??= line[5..].Trim(); | ||||
|             } | ||||
|             else if (line.StartsWith("Version:", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 version ??= line[8..].Trim(); | ||||
|             } | ||||
|  | ||||
|             if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(version)) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return (name, version); | ||||
|     } | ||||
|  | ||||
|     private IReadOnlyList<string> EnumerateMatchingFiles(string rootPath) | ||||
|     { | ||||
|         var files = new List<string>(); | ||||
|         var stack = new Stack<string>(); | ||||
|         stack.Push(rootPath); | ||||
|  | ||||
|         while (stack.Count > 0) | ||||
|         { | ||||
|             var current = stack.Pop(); | ||||
|             foreach (var directory in Directory.EnumerateDirectories(current)) | ||||
|             { | ||||
|                 stack.Push(directory); | ||||
|             } | ||||
|  | ||||
|             foreach (var file in Directory.EnumerateFiles(current)) | ||||
|             { | ||||
|                 var relative = Path.GetRelativePath(rootPath, file).Replace('\\', '/'); | ||||
|                 if (_matcher.IsMatch(relative)) | ||||
|                 { | ||||
|                     files.Add(file); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return files; | ||||
|     } | ||||
|  | ||||
|     private static Regex GlobToRegex(string pattern) | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(pattern)) | ||||
|         { | ||||
|             throw new ArgumentException("Glob pattern is required.", nameof(pattern)); | ||||
|         } | ||||
|  | ||||
|         var normalized = pattern.Replace("\\", "/"); | ||||
|         normalized = normalized.Replace("**", "\u0001"); | ||||
|         normalized = normalized.Replace("*", "\u0002"); | ||||
|  | ||||
|         var escaped = Regex.Escape(normalized); | ||||
|         escaped = escaped.Replace("\u0001/", "(?:.*/)?", StringComparison.Ordinal); | ||||
|         escaped = escaped.Replace("\u0001", ".*", StringComparison.Ordinal); | ||||
|         escaped = escaped.Replace("\u0002", "[^/]*", StringComparison.Ordinal); | ||||
|  | ||||
|         return new Regex("^" + escaped + "$", RegexOptions.Compiled | RegexOptions.CultureInvariant); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user