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:
		@@ -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