feat: add security sink detection patterns for JavaScript/TypeScript

- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
This commit is contained in:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -0,0 +1,216 @@
using Microsoft.AspNetCore.Mvc;
using StellaOps.VexHub.Core;
using StellaOps.VexHub.Core.Models;
using StellaOps.VexHub.WebService.Models;
namespace StellaOps.VexHub.WebService.Extensions;
/// <summary>
/// Extensions for mapping VexHub API endpoints.
/// </summary>
public static class VexHubEndpointExtensions
{
/// <summary>
/// Maps all VexHub API endpoints.
/// </summary>
public static WebApplication MapVexHubEndpoints(this WebApplication app)
{
var vexGroup = app.MapGroup("/api/v1/vex")
.WithTags("VEX");
// GET /api/v1/vex/cve/{cve-id}
vexGroup.MapGet("/cve/{cveId}", GetByCve)
.WithName("GetVexByCve")
.WithDescription("Get VEX statements for a CVE ID")
.Produces<VexStatementsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
// GET /api/v1/vex/package/{purl}
vexGroup.MapGet("/package/{purl}", GetByPackage)
.WithName("GetVexByPackage")
.WithDescription("Get VEX statements for a package PURL")
.Produces<VexStatementsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
// GET /api/v1/vex/source/{source-id}
vexGroup.MapGet("/source/{sourceId}", GetBySource)
.WithName("GetVexBySource")
.WithDescription("Get VEX statements from a specific source")
.Produces<VexStatementsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
// GET /api/v1/vex/statement/{id}
vexGroup.MapGet("/statement/{id:guid}", GetById)
.WithName("GetVexStatement")
.WithDescription("Get a specific VEX statement by ID")
.Produces<AggregatedVexStatement>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
// GET /api/v1/vex/search
vexGroup.MapGet("/search", Search)
.WithName("SearchVex")
.WithDescription("Search VEX statements with filters")
.Produces<VexSearchResponse>(StatusCodes.Status200OK);
// GET /api/v1/vex/stats
vexGroup.MapGet("/stats", GetStats)
.WithName("GetVexStats")
.WithDescription("Get VEX hub statistics")
.Produces<VexHubStats>(StatusCodes.Status200OK);
// GET /api/v1/vex/index
vexGroup.MapGet("/index", GetIndex)
.WithName("GetVexIndex")
.WithDescription("Get VEX hub index manifest for tool integration")
.Produces<VexIndexManifest>(StatusCodes.Status200OK);
return app;
}
private static async Task<IResult> GetByCve(
string cveId,
[FromQuery] int? limit,
[FromQuery] int? offset,
IVexStatementRepository repository,
CancellationToken cancellationToken)
{
var statements = await repository.GetByCveAsync(cveId, limit ?? 100, offset ?? 0, cancellationToken);
if (statements.Count == 0)
return Results.NotFound(new { Message = $"No VEX statements found for CVE {cveId}" });
return Results.Ok(new VexStatementsResponse
{
Statements = statements,
TotalCount = statements.Count,
QueryType = "cve",
QueryValue = cveId
});
}
private static async Task<IResult> GetByPackage(
string purl,
[FromQuery] int? limit,
[FromQuery] int? offset,
IVexStatementRepository repository,
CancellationToken cancellationToken)
{
// URL decode the PURL
var decodedPurl = Uri.UnescapeDataString(purl);
var statements = await repository.GetByPackageAsync(decodedPurl, limit ?? 100, offset ?? 0, cancellationToken);
if (statements.Count == 0)
return Results.NotFound(new { Message = $"No VEX statements found for package {decodedPurl}" });
return Results.Ok(new VexStatementsResponse
{
Statements = statements,
TotalCount = statements.Count,
QueryType = "package",
QueryValue = decodedPurl
});
}
private static async Task<IResult> GetBySource(
string sourceId,
[FromQuery] int? limit,
[FromQuery] int? offset,
IVexStatementRepository repository,
CancellationToken cancellationToken)
{
var statements = await repository.GetBySourceAsync(sourceId, limit ?? 100, offset ?? 0, cancellationToken);
if (statements.Count == 0)
return Results.NotFound(new { Message = $"No VEX statements found for source {sourceId}" });
return Results.Ok(new VexStatementsResponse
{
Statements = statements,
TotalCount = statements.Count,
QueryType = "source",
QueryValue = sourceId
});
}
private static async Task<IResult> GetById(
Guid id,
IVexStatementRepository repository,
CancellationToken cancellationToken)
{
var statement = await repository.GetByIdAsync(id, cancellationToken);
if (statement is null)
return Results.NotFound(new { Message = $"VEX statement {id} not found" });
return Results.Ok(statement);
}
private static async Task<IResult> Search(
[FromQuery] string? sourceId,
[FromQuery] string? vulnerabilityId,
[FromQuery] string? productKey,
[FromQuery] string? status,
[FromQuery] bool? isFlagged,
[FromQuery] int? limit,
[FromQuery] int? offset,
IVexStatementRepository repository,
CancellationToken cancellationToken)
{
var filter = new VexStatementFilter
{
SourceId = sourceId,
VulnerabilityId = vulnerabilityId,
ProductKey = productKey,
IsFlagged = isFlagged
};
var statements = await repository.SearchAsync(filter, limit ?? 100, offset ?? 0, cancellationToken);
var totalCount = await repository.GetCountAsync(filter, cancellationToken);
return Results.Ok(new VexSearchResponse
{
Statements = statements,
TotalCount = totalCount,
Limit = limit ?? 100,
Offset = offset ?? 0
});
}
private static async Task<IResult> GetStats(
IVexStatementRepository repository,
CancellationToken cancellationToken)
{
var totalCount = await repository.GetCountAsync(cancellationToken: cancellationToken);
var verifiedCount = await repository.GetCountAsync(
new VexStatementFilter { VerificationStatus = VerificationStatus.Verified },
cancellationToken);
var flaggedCount = await repository.GetCountAsync(
new VexStatementFilter { IsFlagged = true },
cancellationToken);
return Results.Ok(new VexHubStats
{
TotalStatements = totalCount,
VerifiedStatements = verifiedCount,
FlaggedStatements = flaggedCount,
GeneratedAt = DateTimeOffset.UtcNow
});
}
private static IResult GetIndex()
{
return Results.Ok(new VexIndexManifest
{
Version = "1.0",
LastUpdated = DateTimeOffset.UtcNow,
Endpoints = new VexIndexEndpoints
{
ByCve = "/api/v1/vex/cve/{cve}",
ByPackage = "/api/v1/vex/package/{purl}",
BySource = "/api/v1/vex/source/{source-id}",
Search = "/api/v1/vex/search",
Stats = "/api/v1/vex/stats"
}
});
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
namespace StellaOps.VexHub.WebService.Extensions;
/// <summary>
/// Service collection extensions for VexHub web service.
/// </summary>
public static class VexHubWebServiceExtensions
{
/// <summary>
/// Adds VexHub web service dependencies.
/// </summary>
public static IServiceCollection AddVexHubWebService(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddControllers();
return services;
}
}

View File

@@ -0,0 +1,135 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
namespace StellaOps.VexHub.WebService.Middleware;
/// <summary>
/// Authentication handler for API key authentication.
/// </summary>
public sealed class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private const string ApiKeyHeaderName = "X-Api-Key";
private const string ApiKeyQueryParamName = "api_key";
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Try to get API key from header first
if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeader))
{
// Fall back to query parameter
if (!Request.Query.TryGetValue(ApiKeyQueryParamName, out var apiKeyQuery))
{
// No API key provided - allow anonymous access for public endpoints
if (Options.AllowAnonymous)
{
return Task.FromResult(AuthenticateResult.NoResult());
}
return Task.FromResult(AuthenticateResult.Fail("API key is required"));
}
apiKeyHeader = apiKeyQuery;
}
var providedApiKey = apiKeyHeader.ToString();
if (string.IsNullOrWhiteSpace(providedApiKey))
{
return Task.FromResult(AuthenticateResult.Fail("API key is empty"));
}
// Validate the API key
var apiKeyInfo = ValidateApiKey(providedApiKey);
if (apiKeyInfo is null)
{
return Task.FromResult(AuthenticateResult.Fail("Invalid API key"));
}
// Create claims identity
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, apiKeyInfo.ClientId),
new(ClaimTypes.Name, apiKeyInfo.ClientName),
new("api_key_id", apiKeyInfo.KeyId)
};
foreach (var scope in apiKeyInfo.Scopes)
{
claims.Add(new Claim("scope", scope));
}
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
private ApiKeyInfo? ValidateApiKey(string apiKey)
{
// Check against configured API keys
if (Options.ApiKeys.TryGetValue(apiKey, out var keyInfo))
{
return keyInfo;
}
return null;
}
}
/// <summary>
/// Options for API key authentication.
/// </summary>
public sealed class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
/// <summary>
/// Dictionary of valid API keys to their info.
/// </summary>
public Dictionary<string, ApiKeyInfo> ApiKeys { get; set; } = new();
/// <summary>
/// Whether to allow anonymous access when no API key is provided.
/// </summary>
public bool AllowAnonymous { get; set; } = true;
}
/// <summary>
/// Information about an API key.
/// </summary>
public sealed class ApiKeyInfo
{
/// <summary>
/// Unique identifier for this key.
/// </summary>
public required string KeyId { get; init; }
/// <summary>
/// Client identifier.
/// </summary>
public required string ClientId { get; init; }
/// <summary>
/// Human-readable client name.
/// </summary>
public required string ClientName { get; init; }
/// <summary>
/// Scopes granted to this key.
/// </summary>
public IReadOnlyList<string> Scopes { get; init; } = Array.Empty<string>();
/// <summary>
/// Rate limit override for this key (requests per minute).
/// </summary>
public int? RateLimitPerMinute { get; init; }
}

View File

@@ -0,0 +1,232 @@
using System.Collections.Concurrent;
using System.Net;
using Microsoft.Extensions.Options;
using StellaOps.VexHub.Core.Models;
namespace StellaOps.VexHub.WebService.Middleware;
/// <summary>
/// Middleware for rate limiting API requests using a sliding window algorithm.
/// </summary>
public sealed class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RateLimitingMiddleware> _logger;
private readonly VexHubOptions _options;
private readonly ConcurrentDictionary<string, RateLimitEntry> _rateLimits = new();
private readonly Timer _cleanupTimer;
public RateLimitingMiddleware(
RequestDelegate next,
IOptions<VexHubOptions> options,
ILogger<RateLimitingMiddleware> logger)
{
_next = next;
_options = options.Value;
_logger = logger;
// Clean up old entries every minute
_cleanupTimer = new Timer(CleanupOldEntries, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
public async Task InvokeAsync(HttpContext context)
{
// Skip rate limiting for health checks
if (context.Request.Path.StartsWithSegments("/health"))
{
await _next(context);
return;
}
var clientId = GetClientIdentifier(context);
var rateLimit = GetRateLimitForClient(context);
if (rateLimit <= 0)
{
// Rate limiting disabled
await _next(context);
return;
}
var entry = _rateLimits.GetOrAdd(clientId, _ => new RateLimitEntry());
var now = DateTimeOffset.UtcNow;
// Clean old requests outside the window
entry.CleanOldRequests(now);
// Check if rate limit exceeded
if (entry.RequestCount >= rateLimit)
{
var resetTime = entry.GetResetTime();
var retryAfter = (int)Math.Ceiling((resetTime - now).TotalSeconds);
context.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
context.Response.Headers["Retry-After"] = retryAfter.ToString();
context.Response.Headers["X-RateLimit-Limit"] = rateLimit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] = "0";
context.Response.Headers["X-RateLimit-Reset"] = resetTime.ToUnixTimeSeconds().ToString();
_logger.LogWarning(
"Rate limit exceeded for client {ClientId}. Limit: {Limit}, Retry after: {RetryAfter}s",
clientId,
rateLimit,
retryAfter);
await context.Response.WriteAsJsonAsync(new
{
error = "rate_limit_exceeded",
message = $"Rate limit exceeded. Try again in {retryAfter} seconds.",
retryAfter
});
return;
}
// Record this request
entry.RecordRequest(now);
// Add rate limit headers
context.Response.OnStarting(() =>
{
var remaining = Math.Max(0, rateLimit - entry.RequestCount);
context.Response.Headers["X-RateLimit-Limit"] = rateLimit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] = remaining.ToString();
context.Response.Headers["X-RateLimit-Reset"] = entry.GetResetTime().ToUnixTimeSeconds().ToString();
return Task.CompletedTask;
});
await _next(context);
}
private string GetClientIdentifier(HttpContext context)
{
// First try to get from authenticated user
if (context.User.Identity?.IsAuthenticated == true)
{
var clientId = context.User.FindFirst("api_key_id")?.Value;
if (!string.IsNullOrEmpty(clientId))
{
return $"key:{clientId}";
}
}
// Fall back to IP address
var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
// Check for forwarded IP
if (context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwardedFor))
{
var firstIp = forwardedFor.ToString().Split(',').FirstOrDefault()?.Trim();
if (!string.IsNullOrEmpty(firstIp))
{
ipAddress = firstIp;
}
}
return $"ip:{ipAddress}";
}
private int GetRateLimitForClient(HttpContext context)
{
// Check for API key with custom rate limit
if (context.User.Identity?.IsAuthenticated == true)
{
// API key authenticated clients could have higher limits
// This would be retrieved from the API key info
// For now, use 2x the default rate limit for authenticated clients
return _options.Distribution.RateLimitPerMinute * 2;
}
return _options.Distribution.RateLimitPerMinute;
}
private void CleanupOldEntries(object? state)
{
var now = DateTimeOffset.UtcNow;
var expiredKeys = _rateLimits
.Where(kvp => (now - kvp.Value.LastRequestTime).TotalMinutes > 5)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expiredKeys)
{
_rateLimits.TryRemove(key, out _);
}
if (expiredKeys.Count > 0)
{
_logger.LogDebug("Cleaned up {Count} expired rate limit entries", expiredKeys.Count);
}
}
}
/// <summary>
/// Tracks request counts for rate limiting using a sliding window.
/// </summary>
internal sealed class RateLimitEntry
{
private readonly object _lock = new();
private readonly Queue<DateTimeOffset> _requestTimes = new();
private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
public int RequestCount
{
get
{
lock (_lock)
{
return _requestTimes.Count;
}
}
}
public DateTimeOffset LastRequestTime { get; private set; } = DateTimeOffset.UtcNow;
public void RecordRequest(DateTimeOffset time)
{
lock (_lock)
{
_requestTimes.Enqueue(time);
LastRequestTime = time;
}
}
public void CleanOldRequests(DateTimeOffset now)
{
lock (_lock)
{
var windowStart = now - Window;
while (_requestTimes.Count > 0 && _requestTimes.Peek() < windowStart)
{
_requestTimes.Dequeue();
}
}
}
public DateTimeOffset GetResetTime()
{
lock (_lock)
{
if (_requestTimes.Count == 0)
{
return DateTimeOffset.UtcNow;
}
return _requestTimes.Peek() + Window;
}
}
}
/// <summary>
/// Extensions for adding rate limiting to the application.
/// </summary>
public static class RateLimitingExtensions
{
/// <summary>
/// Adds the rate limiting middleware to the pipeline.
/// </summary>
public static IApplicationBuilder UseVexHubRateLimiting(this IApplicationBuilder app)
{
return app.UseMiddleware<RateLimitingMiddleware>();
}
}

View File

@@ -0,0 +1,58 @@
using StellaOps.VexHub.Core.Models;
namespace StellaOps.VexHub.WebService.Models;
/// <summary>
/// Response containing VEX statements.
/// </summary>
public sealed class VexStatementsResponse
{
public required IReadOnlyList<AggregatedVexStatement> Statements { get; init; }
public required int TotalCount { get; init; }
public required string QueryType { get; init; }
public required string QueryValue { get; init; }
}
/// <summary>
/// Response for VEX search queries.
/// </summary>
public sealed class VexSearchResponse
{
public required IReadOnlyList<AggregatedVexStatement> Statements { get; init; }
public required long TotalCount { get; init; }
public required int Limit { get; init; }
public required int Offset { get; init; }
}
/// <summary>
/// VEX Hub statistics.
/// </summary>
public sealed class VexHubStats
{
public required long TotalStatements { get; init; }
public required long VerifiedStatements { get; init; }
public required long FlaggedStatements { get; init; }
public required DateTimeOffset GeneratedAt { get; init; }
}
/// <summary>
/// VEX Hub index manifest for tool integration.
/// </summary>
public sealed class VexIndexManifest
{
public required string Version { get; init; }
public required DateTimeOffset LastUpdated { get; init; }
public required VexIndexEndpoints Endpoints { get; init; }
}
/// <summary>
/// VEX Hub API endpoints.
/// </summary>
public sealed class VexIndexEndpoints
{
public required string ByCve { get; init; }
public required string ByPackage { get; init; }
public required string BySource { get; init; }
public required string Search { get; init; }
public required string Stats { get; init; }
}

View File

@@ -0,0 +1,73 @@
using Serilog;
using StellaOps.VexHub.Core.Extensions;
using StellaOps.VexHub.Storage.Postgres.Extensions;
using StellaOps.VexHub.WebService.Extensions;
using StellaOps.VexHub.WebService.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.CreateLogger();
builder.Host.UseSerilog();
// Add services to the container
builder.Services.AddVexHubCore(builder.Configuration);
builder.Services.AddVexHubPostgres(builder.Configuration);
builder.Services.AddVexHubWebService(builder.Configuration);
// Add authentication
builder.Services.AddAuthentication("ApiKey")
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", options =>
{
options.AllowAnonymous = true; // Allow anonymous for public read endpoints
// API keys can be configured via configuration
var apiKeysSection = builder.Configuration.GetSection("VexHub:ApiKeys");
foreach (var keySection in apiKeysSection.GetChildren())
{
var key = keySection.Key;
options.ApiKeys[key] = new ApiKeyInfo
{
KeyId = keySection["KeyId"] ?? key,
ClientId = keySection["ClientId"] ?? "unknown",
ClientName = keySection["ClientName"] ?? "Unknown Client",
Scopes = keySection.GetSection("Scopes").Get<string[]>() ?? Array.Empty<string>(),
RateLimitPerMinute = keySection.GetValue<int?>("RateLimitPerMinute")
};
}
});
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseSerilogRequestLogging();
// Add rate limiting middleware
app.UseVexHubRateLimiting();
// Add authentication and authorization
app.UseAuthentication();
app.UseAuthorization();
// Map API endpoints
app.MapVexHubEndpoints();
// Health check
app.MapGet("/health", () => Results.Ok(new { Status = "Healthy", Service = "VexHub" }))
.WithName("HealthCheck")
.WithTags("Health");
app.Run();

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<RootNamespace>StellaOps.VexHub.WebService</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../__Libraries/StellaOps.VexHub.Core/StellaOps.VexHub.Core.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.VexHub.Storage.Postgres/StellaOps.VexHub.Storage.Postgres.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
<ProjectReference Include="../../VexLens/StellaOps.VexLens/StellaOps.VexLens.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Information"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug"
}
}
}

View File

@@ -0,0 +1,54 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
},
"VexHub": {
"DefaultPollingIntervalSeconds": 3600,
"MaxConcurrentPolls": 4,
"StaleStatementAgeDays": 365,
"AutoResolveLowSeverityConflicts": true,
"StoreRawStatements": true,
"MaxApiPageSize": 1000,
"DefaultApiPageSize": 100,
"EnableSignatureVerification": true,
"Ingestion": {
"EnableDeduplication": true,
"EnableConflictDetection": true,
"BatchSize": 500,
"FetchTimeoutSeconds": 300,
"MaxRetries": 3
},
"Distribution": {
"EnableBulkExport": true,
"EnableWebhooks": true,
"CacheDurationSeconds": 300,
"RateLimitPerMinute": 60
}
},
"Postgres": {
"ConnectionString": "Host=localhost;Port=5432;Database=stellaops;Username=postgres;Password=postgres",
"SchemaName": "vexhub"
},
"AllowedHosts": "*"
}