Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.BinaryIndex.Contracts.Resolution;
|
||||
using StellaOps.BinaryIndex.Core.Resolution;
|
||||
|
||||
namespace StellaOps.BinaryIndex.WebService.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for binary vulnerability resolution.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/resolve")]
|
||||
[Produces("application/json")]
|
||||
public sealed class ResolutionController : ControllerBase
|
||||
{
|
||||
private readonly IResolutionService _resolutionService;
|
||||
private readonly ILogger<ResolutionController> _logger;
|
||||
|
||||
public ResolutionController(
|
||||
IResolutionService resolutionService,
|
||||
ILogger<ResolutionController> logger)
|
||||
{
|
||||
_resolutionService = resolutionService ?? throw new ArgumentNullException(nameof(resolutionService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve vulnerability status for a single binary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Accepts binary identity (Build-ID, hashes, fingerprint) and returns resolution status
|
||||
/// with evidence. Results may be cached for performance.
|
||||
///
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/v1/resolve/vuln
|
||||
/// {
|
||||
/// "package": "pkg:deb/debian/openssl@3.0.7",
|
||||
/// "build_id": "abc123def456789...",
|
||||
/// "hashes": {
|
||||
/// "file_sha256": "sha256:e3b0c44...",
|
||||
/// "text_sha256": "sha256:abc123..."
|
||||
/// },
|
||||
/// "distro_release": "debian:bookworm"
|
||||
/// }
|
||||
/// </remarks>
|
||||
/// <param name="request">Resolution request with binary identity.</param>
|
||||
/// <param name="bypassCache">If true, skip cache and perform fresh lookup.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Resolution response with status and evidence.</returns>
|
||||
/// <response code="200">Returns the resolution result.</response>
|
||||
/// <response code="400">Invalid request parameters.</response>
|
||||
/// <response code="404">Binary not found in index.</response>
|
||||
[HttpPost("vuln")]
|
||||
[ProducesResponseType<VulnResolutionResponse>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<VulnResolutionResponse>> ResolveVulnerabilityAsync(
|
||||
[FromBody] VulnResolutionRequest request,
|
||||
[FromQuery] bool bypassCache = false,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
return BadRequest(CreateProblem("Request body is required.", "InvalidRequest"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Package))
|
||||
{
|
||||
return BadRequest(CreateProblem("Package identifier is required.", "MissingPackage"));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Resolving vulnerability for package {Package}, CVE: {CveId}",
|
||||
request.Package, request.CveId ?? "(all)");
|
||||
|
||||
try
|
||||
{
|
||||
var options = new ResolutionOptions
|
||||
{
|
||||
BypassCache = bypassCache,
|
||||
IncludeDsseAttestation = true
|
||||
};
|
||||
|
||||
var result = await _resolutionService.ResolveAsync(request, options, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to resolve vulnerability for package {Package}", request.Package);
|
||||
return StatusCode(500, CreateProblem("Internal server error during resolution.", "ResolutionError"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve vulnerability status for multiple binaries in batch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Processes multiple resolution requests in parallel for efficiency.
|
||||
/// Maximum batch size is 500 items.
|
||||
///
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/v1/resolve/vuln/batch
|
||||
/// {
|
||||
/// "items": [
|
||||
/// { "package": "pkg:deb/debian/openssl@3.0.7", "build_id": "..." },
|
||||
/// { "package": "pkg:deb/debian/libcurl@7.88.1", "build_id": "..." }
|
||||
/// ],
|
||||
/// "options": {
|
||||
/// "bypass_cache": false,
|
||||
/// "include_dsse_attestation": true
|
||||
/// }
|
||||
/// }
|
||||
/// </remarks>
|
||||
/// <param name="request">Batch resolution request.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Batch resolution response with all results.</returns>
|
||||
/// <response code="200">Returns the batch resolution results.</response>
|
||||
/// <response code="400">Invalid request parameters.</response>
|
||||
[HttpPost("vuln/batch")]
|
||||
[ProducesResponseType<BatchVulnResolutionResponse>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<BatchVulnResolutionResponse>> ResolveBatchAsync(
|
||||
[FromBody] BatchVulnResolutionRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
return BadRequest(CreateProblem("Request body is required.", "InvalidRequest"));
|
||||
}
|
||||
|
||||
if (request.Items is null || request.Items.Count == 0)
|
||||
{
|
||||
return BadRequest(CreateProblem("At least one item is required.", "EmptyBatch"));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Processing batch resolution for {Count} items", request.Items.Count);
|
||||
|
||||
try
|
||||
{
|
||||
var options = new ResolutionOptions
|
||||
{
|
||||
BypassCache = request.Options?.BypassCache ?? false,
|
||||
IncludeDsseAttestation = request.Options?.IncludeDsseAttestation ?? true
|
||||
};
|
||||
|
||||
var result = await _resolutionService.ResolveBatchAsync(request, options, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to process batch resolution");
|
||||
return StatusCode(500, CreateProblem("Internal server error during batch resolution.", "BatchResolutionError"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Health check endpoint.
|
||||
/// </summary>
|
||||
[HttpGet("health")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IActionResult Health()
|
||||
{
|
||||
return Ok(new { status = "healthy", timestamp = DateTimeOffset.UtcNow });
|
||||
}
|
||||
|
||||
private static ProblemDetails CreateProblem(string detail, string type)
|
||||
{
|
||||
return new ProblemDetails
|
||||
{
|
||||
Title = "Resolution Error",
|
||||
Detail = detail,
|
||||
Type = $"https://stellaops.dev/errors/{type}",
|
||||
Status = 400
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RateLimitingMiddleware.cs
|
||||
// Sprint: SPRINT_1227_0001_0002_BE_resolution_api
|
||||
// Task: T10 — Rate limiting for resolution API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.BinaryIndex.WebService.Telemetry;
|
||||
|
||||
namespace StellaOps.BinaryIndex.WebService.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Rate limiting middleware for the resolution API.
|
||||
/// Implements sliding window rate limiting per tenant.
|
||||
/// </summary>
|
||||
public sealed class RateLimitingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<RateLimitingMiddleware> _logger;
|
||||
private readonly RateLimitingOptions _options;
|
||||
private readonly ResolutionTelemetry? _telemetry;
|
||||
private readonly ConcurrentDictionary<string, SlidingWindowCounter> _counters = new();
|
||||
|
||||
public RateLimitingMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<RateLimitingMiddleware> logger,
|
||||
IOptions<RateLimitingOptions> options,
|
||||
ResolutionTelemetry? telemetry = null)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_telemetry = telemetry;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
// Only apply to resolution endpoints
|
||||
if (!context.Request.Path.StartsWithSegments("/api/v1/resolve"))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var tenantId = GetTenantId(context);
|
||||
var clientIp = GetClientIp(context);
|
||||
var rateLimitKey = $"{tenantId}:{clientIp}";
|
||||
|
||||
var counter = _counters.GetOrAdd(rateLimitKey, _ => new SlidingWindowCounter(_options.WindowSize));
|
||||
|
||||
if (!counter.TryIncrement(_options.MaxRequests))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Rate limit exceeded for tenant {TenantId} from {ClientIp}",
|
||||
tenantId, clientIp);
|
||||
|
||||
_telemetry?.RecordRateLimited(tenantId);
|
||||
|
||||
context.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
|
||||
context.Response.Headers["Retry-After"] = _options.RetryAfterSeconds.ToString();
|
||||
context.Response.Headers["X-RateLimit-Limit"] = _options.MaxRequests.ToString();
|
||||
context.Response.Headers["X-RateLimit-Remaining"] = "0";
|
||||
context.Response.Headers["X-RateLimit-Reset"] = DateTimeOffset.UtcNow
|
||||
.AddSeconds(_options.RetryAfterSeconds).ToUnixTimeSeconds().ToString();
|
||||
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
error = "rate_limit_exceeded",
|
||||
message = $"Rate limit of {_options.MaxRequests} requests per {_options.WindowSize.TotalSeconds} seconds exceeded",
|
||||
retry_after_seconds = _options.RetryAfterSeconds
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Add rate limit headers
|
||||
var remaining = Math.Max(0, _options.MaxRequests - counter.Count);
|
||||
context.Response.Headers["X-RateLimit-Limit"] = _options.MaxRequests.ToString();
|
||||
context.Response.Headers["X-RateLimit-Remaining"] = remaining.ToString();
|
||||
context.Response.Headers["X-RateLimit-Reset"] = counter.WindowReset.ToUnixTimeSeconds().ToString();
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private static string GetTenantId(HttpContext context)
|
||||
{
|
||||
// Try to get tenant from header, claim, or default
|
||||
if (context.Request.Headers.TryGetValue("X-Tenant-Id", out var tenantHeader))
|
||||
{
|
||||
return tenantHeader.ToString();
|
||||
}
|
||||
|
||||
var claim = context.User?.FindFirst("tenant_id");
|
||||
if (claim != null)
|
||||
{
|
||||
return claim.Value;
|
||||
}
|
||||
|
||||
return "default";
|
||||
}
|
||||
|
||||
private static string GetClientIp(HttpContext context)
|
||||
{
|
||||
// Check for forwarded headers first
|
||||
if (context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwarded))
|
||||
{
|
||||
var ips = forwarded.ToString().Split(',');
|
||||
if (ips.Length > 0)
|
||||
{
|
||||
return ips[0].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sliding window rate limit counter.
|
||||
/// </summary>
|
||||
internal sealed class SlidingWindowCounter
|
||||
{
|
||||
private readonly TimeSpan _windowSize;
|
||||
private readonly object _lock = new();
|
||||
private int _count;
|
||||
private DateTimeOffset _windowStart;
|
||||
|
||||
public SlidingWindowCounter(TimeSpan windowSize)
|
||||
{
|
||||
_windowSize = windowSize;
|
||||
_windowStart = DateTimeOffset.UtcNow;
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ResetIfNeeded();
|
||||
return _count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset WindowReset => _windowStart + _windowSize;
|
||||
|
||||
public bool TryIncrement(int maxRequests)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ResetIfNeeded();
|
||||
|
||||
if (_count >= maxRequests)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_count++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetIfNeeded()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (now >= _windowStart + _windowSize)
|
||||
{
|
||||
_windowStart = now;
|
||||
_count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate limiting configuration options.
|
||||
/// </summary>
|
||||
public sealed class RateLimitingOptions
|
||||
{
|
||||
/// <summary>Maximum requests per window.</summary>
|
||||
public int MaxRequests { get; set; } = 100;
|
||||
|
||||
/// <summary>Sliding window size.</summary>
|
||||
public TimeSpan WindowSize { get; set; } = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>Retry-After header value in seconds.</summary>
|
||||
public int RetryAfterSeconds { get; set; } = 60;
|
||||
|
||||
/// <summary>Enable rate limiting.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for rate limiting.
|
||||
/// </summary>
|
||||
public static class RateLimitingExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseResolutionRateLimiting(this IApplicationBuilder app)
|
||||
{
|
||||
return app.UseMiddleware<RateLimitingMiddleware>();
|
||||
}
|
||||
|
||||
public static IServiceCollection AddResolutionRateLimiting(
|
||||
this IServiceCollection services,
|
||||
Action<RateLimitingOptions>? configure = null)
|
||||
{
|
||||
if (configure != null)
|
||||
{
|
||||
services.Configure(configure);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.Configure<RateLimitingOptions>(_ => { });
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
49
src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs
Normal file
49
src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using StellaOps.BinaryIndex.Cache;
|
||||
using StellaOps.BinaryIndex.Core.Resolution;
|
||||
using StellaOps.BinaryIndex.VexBridge;
|
||||
using StackExchange.Redis;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// Configure options
|
||||
builder.Services.Configure<ResolutionServiceOptions>(
|
||||
builder.Configuration.GetSection(ResolutionServiceOptions.SectionName));
|
||||
builder.Services.Configure<ResolutionCacheOptions>(
|
||||
builder.Configuration.GetSection(ResolutionCacheOptions.SectionName));
|
||||
|
||||
// Add Redis/Valkey connection
|
||||
var redisConnectionString = builder.Configuration.GetConnectionString("Redis") ?? "localhost:6379";
|
||||
builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
|
||||
ConnectionMultiplexer.Connect(redisConnectionString));
|
||||
|
||||
// Add services
|
||||
builder.Services.AddSingleton<IResolutionCacheService, ResolutionCacheService>();
|
||||
builder.Services.AddScoped<IResolutionService, ResolutionService>();
|
||||
|
||||
// Add VexBridge
|
||||
builder.Services.AddBinaryVexBridge(builder.Configuration);
|
||||
|
||||
// Add health checks
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddRedis(redisConnectionString, name: "redis");
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"StellaOps.BinaryIndex.WebService": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:49948;http://localhost:49949"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<Description>BinaryIndex WebService - Resolution API for binary vulnerability lookup</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Redis" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.Core/StellaOps.BinaryIndex.Core.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.Cache/StellaOps.BinaryIndex.Cache.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.Contracts/StellaOps.BinaryIndex.Contracts.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.VexBridge/StellaOps.BinaryIndex.VexBridge.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,218 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ResolutionTelemetry.cs
|
||||
// Sprint: SPRINT_1227_0001_0002_BE_resolution_api
|
||||
// Task: T11 — Telemetry for resolution API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace StellaOps.BinaryIndex.WebService.Telemetry;
|
||||
|
||||
/// <summary>
|
||||
/// OpenTelemetry instrumentation for binary resolution API.
|
||||
/// </summary>
|
||||
public sealed class ResolutionTelemetry : IDisposable
|
||||
{
|
||||
public const string ServiceName = "StellaOps.BinaryIndex.Resolution";
|
||||
public const string MeterName = "StellaOps.BinaryIndex.Resolution";
|
||||
public const string ActivitySourceName = "StellaOps.BinaryIndex.Resolution";
|
||||
|
||||
private readonly Meter _meter;
|
||||
|
||||
// Counters
|
||||
private readonly Counter<long> _requestsTotal;
|
||||
private readonly Counter<long> _cacheHitsTotal;
|
||||
private readonly Counter<long> _cacheMissesTotal;
|
||||
private readonly Counter<long> _resolutionsTotal;
|
||||
private readonly Counter<long> _errorsTotal;
|
||||
private readonly Counter<long> _rateLimitedTotal;
|
||||
|
||||
// Histograms
|
||||
private readonly Histogram<double> _requestDurationMs;
|
||||
private readonly Histogram<double> _cacheLatencyMs;
|
||||
private readonly Histogram<double> _fingerprintMatchDurationMs;
|
||||
private readonly Histogram<int> _batchSize;
|
||||
private readonly Histogram<double> _confidenceScore;
|
||||
|
||||
// Gauges
|
||||
private readonly UpDownCounter<long> _requestsInProgress;
|
||||
|
||||
public static readonly ActivitySource ActivitySource = new(ActivitySourceName);
|
||||
|
||||
public ResolutionTelemetry(IMeterFactory? meterFactory = null)
|
||||
{
|
||||
_meter = meterFactory?.Create(MeterName) ?? new Meter(MeterName);
|
||||
|
||||
_requestsTotal = _meter.CreateCounter<long>(
|
||||
"binaryindex.resolution.requests.total",
|
||||
unit: "{request}",
|
||||
description: "Total resolution API requests");
|
||||
|
||||
_cacheHitsTotal = _meter.CreateCounter<long>(
|
||||
"binaryindex.resolution.cache.hits.total",
|
||||
unit: "{hit}",
|
||||
description: "Total cache hits");
|
||||
|
||||
_cacheMissesTotal = _meter.CreateCounter<long>(
|
||||
"binaryindex.resolution.cache.misses.total",
|
||||
unit: "{miss}",
|
||||
description: "Total cache misses");
|
||||
|
||||
_resolutionsTotal = _meter.CreateCounter<long>(
|
||||
"binaryindex.resolution.resolutions.total",
|
||||
unit: "{resolution}",
|
||||
description: "Total successful resolutions");
|
||||
|
||||
_errorsTotal = _meter.CreateCounter<long>(
|
||||
"binaryindex.resolution.errors.total",
|
||||
unit: "{error}",
|
||||
description: "Total resolution errors");
|
||||
|
||||
_rateLimitedTotal = _meter.CreateCounter<long>(
|
||||
"binaryindex.resolution.rate_limited.total",
|
||||
unit: "{request}",
|
||||
description: "Total rate-limited requests");
|
||||
|
||||
_requestDurationMs = _meter.CreateHistogram<double>(
|
||||
"binaryindex.resolution.request.duration.ms",
|
||||
unit: "ms",
|
||||
description: "Request duration in milliseconds");
|
||||
|
||||
_cacheLatencyMs = _meter.CreateHistogram<double>(
|
||||
"binaryindex.resolution.cache.latency.ms",
|
||||
unit: "ms",
|
||||
description: "Cache lookup latency in milliseconds");
|
||||
|
||||
_fingerprintMatchDurationMs = _meter.CreateHistogram<double>(
|
||||
"binaryindex.resolution.fingerprint_match.duration.ms",
|
||||
unit: "ms",
|
||||
description: "Fingerprint matching duration in milliseconds");
|
||||
|
||||
_batchSize = _meter.CreateHistogram<int>(
|
||||
"binaryindex.resolution.batch.size",
|
||||
unit: "{item}",
|
||||
description: "Batch request size");
|
||||
|
||||
_confidenceScore = _meter.CreateHistogram<double>(
|
||||
"binaryindex.resolution.confidence",
|
||||
unit: "1",
|
||||
description: "Resolution confidence score distribution");
|
||||
|
||||
_requestsInProgress = _meter.CreateUpDownCounter<long>(
|
||||
"binaryindex.resolution.requests.in_progress",
|
||||
unit: "{request}",
|
||||
description: "Requests currently in progress");
|
||||
}
|
||||
|
||||
public void RecordRequest(string method, string status, TimeSpan duration, bool cacheHit)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ ResolutionTelemetryTags.Method, method },
|
||||
{ ResolutionTelemetryTags.Status, status },
|
||||
{ ResolutionTelemetryTags.CacheHit, cacheHit.ToString().ToLowerInvariant() }
|
||||
};
|
||||
|
||||
_requestsTotal.Add(1, tags);
|
||||
_requestDurationMs.Record(duration.TotalMilliseconds, tags);
|
||||
|
||||
if (cacheHit)
|
||||
_cacheHitsTotal.Add(1, tags);
|
||||
else
|
||||
_cacheMissesTotal.Add(1, tags);
|
||||
}
|
||||
|
||||
public void RecordResolution(string matchType, string resolutionStatus, decimal confidence)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ ResolutionTelemetryTags.MatchType, matchType },
|
||||
{ ResolutionTelemetryTags.ResolutionStatus, resolutionStatus }
|
||||
};
|
||||
|
||||
_resolutionsTotal.Add(1, tags);
|
||||
_confidenceScore.Record((double)confidence, tags);
|
||||
}
|
||||
|
||||
public void RecordError(string errorCode, string method)
|
||||
{
|
||||
_errorsTotal.Add(1, new TagList
|
||||
{
|
||||
{ ResolutionTelemetryTags.ErrorCode, errorCode },
|
||||
{ ResolutionTelemetryTags.Method, method }
|
||||
});
|
||||
}
|
||||
|
||||
public void RecordRateLimited(string tenantId)
|
||||
{
|
||||
_rateLimitedTotal.Add(1, new TagList
|
||||
{
|
||||
{ ResolutionTelemetryTags.TenantId, tenantId }
|
||||
});
|
||||
}
|
||||
|
||||
public void RecordBatchRequest(int size)
|
||||
{
|
||||
_batchSize.Record(size);
|
||||
}
|
||||
|
||||
public void RecordCacheLatency(TimeSpan latency, bool hit)
|
||||
{
|
||||
_cacheLatencyMs.Record(latency.TotalMilliseconds, new TagList
|
||||
{
|
||||
{ ResolutionTelemetryTags.CacheHit, hit.ToString().ToLowerInvariant() }
|
||||
});
|
||||
}
|
||||
|
||||
public void RecordFingerprintMatchDuration(TimeSpan duration, string algorithm)
|
||||
{
|
||||
_fingerprintMatchDurationMs.Record(duration.TotalMilliseconds, new TagList
|
||||
{
|
||||
{ ResolutionTelemetryTags.Algorithm, algorithm }
|
||||
});
|
||||
}
|
||||
|
||||
public void IncrementInProgress() => _requestsInProgress.Add(1);
|
||||
public void DecrementInProgress() => _requestsInProgress.Add(-1);
|
||||
|
||||
public static Activity? StartResolveActivity(string package, string? cveId)
|
||||
{
|
||||
var activity = ActivitySource.StartActivity("Resolution.Resolve");
|
||||
activity?.SetTag("package", package);
|
||||
if (cveId != null) activity?.SetTag("cve_id", cveId);
|
||||
return activity;
|
||||
}
|
||||
|
||||
public static Activity? StartBatchResolveActivity(int count)
|
||||
{
|
||||
var activity = ActivitySource.StartActivity("Resolution.ResolveBatch");
|
||||
activity?.SetTag("batch_size", count);
|
||||
return activity;
|
||||
}
|
||||
|
||||
public void Dispose() => _meter.Dispose();
|
||||
}
|
||||
|
||||
public static class ResolutionTelemetryTags
|
||||
{
|
||||
public const string Method = "method";
|
||||
public const string Status = "status";
|
||||
public const string CacheHit = "cache_hit";
|
||||
public const string MatchType = "match_type";
|
||||
public const string ResolutionStatus = "resolution_status";
|
||||
public const string ErrorCode = "error_code";
|
||||
public const string TenantId = "tenant_id";
|
||||
public const string Algorithm = "algorithm";
|
||||
}
|
||||
|
||||
public static class ResolutionTelemetryExtensions
|
||||
{
|
||||
public static IServiceCollection AddResolutionTelemetry(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<ResolutionTelemetry>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"Redis": "localhost:6379"
|
||||
},
|
||||
"Resolution": {
|
||||
"DefaultCacheTtl": "04:00:00",
|
||||
"MaxBatchSize": 500,
|
||||
"EnableDsseByDefault": true,
|
||||
"MinConfidenceThreshold": 0.70
|
||||
},
|
||||
"ResolutionCache": {
|
||||
"FixedTtl": "24:00:00",
|
||||
"VulnerableTtl": "04:00:00",
|
||||
"UnknownTtl": "01:00:00",
|
||||
"KeyPrefix": "resolution",
|
||||
"EnableEarlyExpiry": true,
|
||||
"EarlyExpiryFactor": 0.1
|
||||
},
|
||||
"VexBridge": {
|
||||
"SignWithDsse": true,
|
||||
"DefaultProviderId": "stellaops.binaryindex",
|
||||
"DefaultStreamId": "binary_resolution",
|
||||
"MinConfidenceThreshold": 0.70,
|
||||
"MaxBatchSize": 1000
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user