sprints work
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user