sprints work

This commit is contained in:
master
2026-01-10 20:32:13 +02:00
parent 0d5eda86fc
commit 17d0631b8e
189 changed files with 40667 additions and 497 deletions

View File

@@ -232,6 +232,220 @@ public sealed class CveSymbolMappingService : ICveSymbolMappingService
return Task.FromResult<IReadOnlyList<string>>([]);
}
/// <inheritdoc />
public Task<IReadOnlyList<CveSymbolMapping>> GetMappingsForCveAsync(string cveId, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var normalizedId = NormalizeCveId(cveId);
// Find all mappings for this CVE (in our in-memory implementation, one CVE = one mapping)
var results = new List<CveSymbolMapping>();
if (_mappings.TryGetValue(normalizedId, out var mapping))
{
results.Add(mapping);
}
return Task.FromResult<IReadOnlyList<CveSymbolMapping>>(results);
}
/// <inheritdoc />
public Task<IReadOnlyList<CveSymbolMapping>> GetMappingsForPackageAsync(string purl, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var normalizedPurl = purl.Trim();
var results = _mappings.Values
.Where(m => m.AffectedPurls.Any(p => p.Equals(normalizedPurl, StringComparison.OrdinalIgnoreCase)))
.ToList();
return Task.FromResult<IReadOnlyList<CveSymbolMapping>>(results);
}
/// <inheritdoc />
public Task<CveSymbolMapping> AddOrUpdateMappingAsync(CveSymbolMapping mapping, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var normalizedId = NormalizeCveId(mapping.CveId);
// Add or update
var result = _mappings.AddOrUpdate(
normalizedId,
mapping,
(_, existing) => existing.Merge(mapping, _timeProvider));
// Update symbol-to-CVE index
foreach (var symbol in mapping.Symbols)
{
var cves = _symbolToCves.GetOrAdd(symbol.Symbol.CanonicalId, _ => new HashSet<string>(StringComparer.OrdinalIgnoreCase));
lock (cves)
{
cves.Add(normalizedId);
}
}
_logger.LogInformation(
"Added/updated mapping for {CveId} with {SymbolCount} symbols",
normalizedId,
mapping.Symbols.Length);
return Task.FromResult(result);
}
/// <inheritdoc />
public async Task<PatchAnalysisResult> AnalyzePatchAsync(
string? commitUrl,
string? diffContent,
CancellationToken ct)
{
if (_patchExtractor is null)
throw new InvalidOperationException("Patch extractor is not configured");
_logger.LogInformation("Analyzing patch: {CommitUrl}", commitUrl ?? "(inline diff)");
PatchAnalysisResult result;
if (!string.IsNullOrWhiteSpace(commitUrl))
{
result = await _patchExtractor.ExtractFromCommitUrlAsync(commitUrl, ct);
}
else if (!string.IsNullOrWhiteSpace(diffContent))
{
result = await _patchExtractor.ExtractFromDiffAsync(diffContent, ct);
}
else
{
throw new ArgumentException("Either commitUrl or diffContent is required");
}
return result;
}
/// <inheritdoc />
public async Task<IReadOnlyList<CveSymbolMapping>> EnrichFromOsvAsync(string cveId, CancellationToken ct)
{
if (_osvEnricher is null)
{
_logger.LogWarning("OSV enricher is not configured, returning empty list");
return [];
}
var normalizedId = NormalizeCveId(cveId);
var enrichment = await _osvEnricher.EnrichAsync(normalizedId, ct);
if (!enrichment.Found)
{
_logger.LogDebug("CVE {CveId} not found in OSV", normalizedId);
return [];
}
// Create mappings from OSV data
var results = new List<CveSymbolMapping>();
foreach (var purl in enrichment.AffectedPurls)
{
var mapping = CveSymbolMapping.Create(
normalizedId,
enrichment.Symbols,
MappingSource.OsvDatabase,
confidence: 0.7,
_timeProvider,
osvAdvisoryId: enrichment.OsvId,
affectedPurls: enrichment.AffectedPurls);
results.Add(mapping);
// Also ingest to update our cache
await IngestMappingAsync(mapping, ct);
}
return results;
}
/// <inheritdoc />
public Task<IReadOnlyList<CveSymbolMapping>> SearchBySymbolAsync(
string symbol,
string? language,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var pattern = symbol.ToLowerInvariant();
var results = new List<CveSymbolMapping>();
foreach (var mapping in _mappings.Values)
{
foreach (var sym in mapping.Symbols)
{
var displayName = sym.Symbol.DisplayName.ToLowerInvariant();
// Match symbol name
if (!displayName.Contains(pattern))
continue;
// Filter by language if specified
if (!string.IsNullOrWhiteSpace(language))
{
// Check if mapping language matches (simplified check)
var mappingLanguage = DetermineLanguageFromSymbol(sym);
if (!string.Equals(mappingLanguage, language, StringComparison.OrdinalIgnoreCase))
continue;
}
results.Add(mapping);
break; // Only add each mapping once
}
}
return Task.FromResult<IReadOnlyList<CveSymbolMapping>>(results);
}
/// <inheritdoc />
public Task<MappingStats> GetStatsAsync(CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var mappings = _mappings.Values.ToList();
var bySource = mappings.GroupBy(m => m.Source.ToString())
.ToDictionary(g => g.Key, g => g.Count());
var byVulnType = mappings.SelectMany(m => m.Symbols)
.GroupBy(s => s.Type.ToString())
.ToDictionary(g => g.Key, g => g.Count());
var stats = new MappingStats
{
TotalMappings = mappings.Count,
UniqueCves = mappings.Select(m => m.CveId).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
UniquePackages = mappings.SelectMany(m => m.AffectedPurls)
.Where(p => !string.IsNullOrWhiteSpace(p))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count(),
BySource = bySource,
ByVulnerabilityType = byVulnType,
AverageConfidence = mappings.Count > 0 ? mappings.Average(m => m.Confidence) : 0,
LastUpdated = mappings.Count > 0 ? mappings.Max(m => m.ExtractedAt) : _timeProvider.GetUtcNow()
};
return Task.FromResult(stats);
}
private static string DetermineLanguageFromSymbol(VulnerableSymbol symbol)
{
// Simple heuristic based on file path or symbol patterns
var filePath = symbol.SourceFile?.ToLowerInvariant() ?? "";
if (filePath.EndsWith(".java", StringComparison.Ordinal)) return "java";
if (filePath.EndsWith(".py", StringComparison.Ordinal)) return "python";
if (filePath.EndsWith(".js", StringComparison.Ordinal)) return "javascript";
if (filePath.EndsWith(".ts", StringComparison.Ordinal)) return "typescript";
if (filePath.EndsWith(".cs", StringComparison.Ordinal)) return "csharp";
if (filePath.EndsWith(".go", StringComparison.Ordinal)) return "go";
if (filePath.EndsWith(".rs", StringComparison.Ordinal)) return "rust";
if (filePath.EndsWith(".rb", StringComparison.Ordinal)) return "ruby";
if (filePath.EndsWith(".php", StringComparison.Ordinal)) return "php";
return "unknown";
}
/// <summary>
/// Gets the total number of mappings.
/// </summary>

View File

@@ -2,6 +2,8 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
namespace StellaOps.Reachability.Core.CveMapping;
/// <summary>
@@ -18,6 +20,22 @@ public interface ICveSymbolMappingService
/// <returns>Mapping if exists, null otherwise.</returns>
Task<CveSymbolMapping?> GetMappingAsync(string cveId, CancellationToken ct);
/// <summary>
/// Gets all mappings for a specific CVE.
/// </summary>
/// <param name="cveId">CVE identifier.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>All mappings for the CVE.</returns>
Task<IReadOnlyList<CveSymbolMapping>> GetMappingsForCveAsync(string cveId, CancellationToken ct);
/// <summary>
/// Gets all mappings for a specific package.
/// </summary>
/// <param name="purl">Package URL.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>All mappings for the package.</returns>
Task<IReadOnlyList<CveSymbolMapping>> GetMappingsForPackageAsync(string purl, CancellationToken ct);
/// <summary>
/// Gets mappings for multiple CVEs in a single call.
/// </summary>
@@ -35,6 +53,14 @@ public interface ICveSymbolMappingService
/// <param name="ct">Cancellation token.</param>
Task IngestMappingAsync(CveSymbolMapping mapping, CancellationToken ct);
/// <summary>
/// Adds or updates a mapping.
/// </summary>
/// <param name="mapping">The mapping to add or update.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The added or updated mapping.</returns>
Task<CveSymbolMapping> AddOrUpdateMappingAsync(CveSymbolMapping mapping, CancellationToken ct);
/// <summary>
/// Extracts a mapping by analyzing a patch commit.
/// </summary>
@@ -47,6 +73,18 @@ public interface ICveSymbolMappingService
string commitUrl,
CancellationToken ct);
/// <summary>
/// Analyzes a patch to extract symbols.
/// </summary>
/// <param name="commitUrl">Optional URL to the patch commit.</param>
/// <param name="diffContent">Optional inline diff content.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Patch analysis result with extracted symbols.</returns>
Task<PatchAnalysisResult> AnalyzePatchAsync(
string? commitUrl,
string? diffContent,
CancellationToken ct);
/// <summary>
/// Enriches an existing mapping with data from OSV.
/// </summary>
@@ -57,6 +95,14 @@ public interface ICveSymbolMappingService
CveSymbolMapping mapping,
CancellationToken ct);
/// <summary>
/// Enriches CVE data from OSV database.
/// </summary>
/// <param name="cveId">CVE identifier to enrich.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Enriched mappings from OSV.</returns>
Task<IReadOnlyList<CveSymbolMapping>> EnrichFromOsvAsync(string cveId, CancellationToken ct);
/// <summary>
/// Searches for mappings by symbol pattern.
/// </summary>
@@ -69,6 +115,18 @@ public interface ICveSymbolMappingService
int limit,
CancellationToken ct);
/// <summary>
/// Searches for mappings by symbol name, optionally filtered by language.
/// </summary>
/// <param name="symbol">Symbol name or pattern.</param>
/// <param name="language">Optional programming language filter.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Matching mappings.</returns>
Task<IReadOnlyList<CveSymbolMapping>> SearchBySymbolAsync(
string symbol,
string? language,
CancellationToken ct);
/// <summary>
/// Gets all CVEs that have mappings for a specific canonical symbol.
/// </summary>
@@ -78,4 +136,25 @@ public interface ICveSymbolMappingService
Task<IReadOnlyList<string>> GetCvesForSymbolAsync(
string canonicalId,
CancellationToken ct);
/// <summary>
/// Gets mapping statistics.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Mapping statistics.</returns>
Task<MappingStats> GetStatsAsync(CancellationToken ct);
}
/// <summary>
/// Statistics about the CVE-symbol mapping corpus.
/// </summary>
public record MappingStats
{
public int TotalMappings { get; init; }
public int UniqueCves { get; init; }
public int UniquePackages { get; init; }
public Dictionary<string, int>? BySource { get; init; }
public Dictionary<string, int>? ByVulnerabilityType { get; init; }
public double AverageConfidence { get; init; }
public DateTimeOffset LastUpdated { get; init; }
}