save checkpoint
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using StellaOps.Reachability.Core.CveMapping;
|
||||
using StellaOps.Reachability.Core.Symbols;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.ReachGraph.WebService.Controllers;
|
||||
@@ -59,25 +60,13 @@ public class CveMappingController : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
var dtos = FlattenMappings(mappings, cveId);
|
||||
|
||||
var response = new CveMappingResponse
|
||||
{
|
||||
CveId = cveId,
|
||||
MappingCount = mappings.Count,
|
||||
Mappings = mappings.Select(m => new CveMappingDto
|
||||
{
|
||||
Purl = m.Purl,
|
||||
Symbol = m.Symbol.Symbol,
|
||||
CanonicalId = m.Symbol.CanonicalId,
|
||||
FilePath = m.Symbol.FilePath,
|
||||
StartLine = m.Symbol.StartLine,
|
||||
EndLine = m.Symbol.EndLine,
|
||||
Source = m.Source.ToString(),
|
||||
Confidence = m.Confidence,
|
||||
VulnerabilityType = m.VulnerabilityType.ToString(),
|
||||
AffectedVersions = m.AffectedVersions.ToList(),
|
||||
FixedVersions = m.FixedVersions.ToList(),
|
||||
EvidenceUri = m.EvidenceUri
|
||||
}).ToList()
|
||||
MappingCount = dtos.Count,
|
||||
Mappings = dtos
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
@@ -110,26 +99,13 @@ public class CveMappingController : ControllerBase
|
||||
|
||||
var mappings = await _mappingService.GetMappingsForPackageAsync(purl, cancellationToken);
|
||||
|
||||
var dtos = FlattenMappings(mappings);
|
||||
|
||||
var response = new PackageMappingsResponse
|
||||
{
|
||||
Purl = purl,
|
||||
MappingCount = mappings.Count,
|
||||
Mappings = mappings.Select(m => new CveMappingDto
|
||||
{
|
||||
CveId = m.CveId,
|
||||
Purl = m.Purl,
|
||||
Symbol = m.Symbol.Symbol,
|
||||
CanonicalId = m.Symbol.CanonicalId,
|
||||
FilePath = m.Symbol.FilePath,
|
||||
StartLine = m.Symbol.StartLine,
|
||||
EndLine = m.Symbol.EndLine,
|
||||
Source = m.Source.ToString(),
|
||||
Confidence = m.Confidence,
|
||||
VulnerabilityType = m.VulnerabilityType.ToString(),
|
||||
AffectedVersions = m.AffectedVersions.ToList(),
|
||||
FixedVersions = m.FixedVersions.ToList(),
|
||||
EvidenceUri = m.EvidenceUri
|
||||
}).ToList()
|
||||
MappingCount = dtos.Count,
|
||||
Mappings = dtos
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
@@ -164,27 +140,14 @@ public class CveMappingController : ControllerBase
|
||||
|
||||
var mappings = await _mappingService.SearchBySymbolAsync(symbol, language, cancellationToken);
|
||||
|
||||
var dtos = FlattenMappings(mappings);
|
||||
|
||||
var response = new SymbolMappingsResponse
|
||||
{
|
||||
Symbol = symbol,
|
||||
Language = language,
|
||||
MappingCount = mappings.Count,
|
||||
Mappings = mappings.Select(m => new CveMappingDto
|
||||
{
|
||||
CveId = m.CveId,
|
||||
Purl = m.Purl,
|
||||
Symbol = m.Symbol.Symbol,
|
||||
CanonicalId = m.Symbol.CanonicalId,
|
||||
FilePath = m.Symbol.FilePath,
|
||||
StartLine = m.Symbol.StartLine,
|
||||
EndLine = m.Symbol.EndLine,
|
||||
Source = m.Source.ToString(),
|
||||
Confidence = m.Confidence,
|
||||
VulnerabilityType = m.VulnerabilityType.ToString(),
|
||||
AffectedVersions = m.AffectedVersions.ToList(),
|
||||
FixedVersions = m.FixedVersions.ToList(),
|
||||
EvidenceUri = m.EvidenceUri
|
||||
}).ToList()
|
||||
MappingCount = dtos.Count,
|
||||
Mappings = dtos
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
@@ -248,44 +211,37 @@ public class CveMappingController : ControllerBase
|
||||
vulnType = VulnerabilityType.Unknown;
|
||||
}
|
||||
|
||||
var mapping = new CveSymbolMapping
|
||||
var canonicalSymbol = CanonicalSymbol.Create(
|
||||
@namespace: "_",
|
||||
type: "_",
|
||||
method: request.Symbol,
|
||||
signature: string.Empty,
|
||||
source: SymbolSource.ManualCuration,
|
||||
purl: request.Purl);
|
||||
|
||||
var vulnerableSymbol = new VulnerableSymbol
|
||||
{
|
||||
CveId = request.CveId,
|
||||
Purl = request.Purl,
|
||||
Symbol = new VulnerableSymbol
|
||||
{
|
||||
Symbol = request.Symbol,
|
||||
CanonicalId = request.CanonicalId,
|
||||
FilePath = request.FilePath,
|
||||
StartLine = request.StartLine,
|
||||
EndLine = request.EndLine
|
||||
},
|
||||
Source = source,
|
||||
Symbol = canonicalSymbol,
|
||||
Type = vulnType,
|
||||
Confidence = request.Confidence ?? 0.5,
|
||||
VulnerabilityType = vulnType,
|
||||
AffectedVersions = request.AffectedVersions?.ToImmutableArray() ?? [],
|
||||
FixedVersions = request.FixedVersions?.ToImmutableArray() ?? [],
|
||||
EvidenceUri = request.EvidenceUri
|
||||
SourceFile = request.FilePath,
|
||||
LineRange = request.StartLine.HasValue && request.EndLine.HasValue
|
||||
? new LineRange(request.StartLine.Value, request.EndLine.Value)
|
||||
: null
|
||||
};
|
||||
|
||||
var mapping = CveSymbolMapping.Create(
|
||||
cveId: request.CveId,
|
||||
symbols: [vulnerableSymbol],
|
||||
source: source,
|
||||
confidence: request.Confidence ?? 0.5,
|
||||
timeProvider: TimeProvider.System,
|
||||
affectedPurls: request.Purl is not null ? [request.Purl] : null);
|
||||
|
||||
var result = await _mappingService.AddOrUpdateMappingAsync(mapping, cancellationToken);
|
||||
|
||||
var response = new CveMappingDto
|
||||
{
|
||||
CveId = result.CveId,
|
||||
Purl = result.Purl,
|
||||
Symbol = result.Symbol.Symbol,
|
||||
CanonicalId = result.Symbol.CanonicalId,
|
||||
FilePath = result.Symbol.FilePath,
|
||||
StartLine = result.Symbol.StartLine,
|
||||
EndLine = result.Symbol.EndLine,
|
||||
Source = result.Source.ToString(),
|
||||
Confidence = result.Confidence,
|
||||
VulnerabilityType = result.VulnerabilityType.ToString(),
|
||||
AffectedVersions = result.AffectedVersions.ToList(),
|
||||
FixedVersions = result.FixedVersions.ToList(),
|
||||
EvidenceUri = result.EvidenceUri
|
||||
};
|
||||
var dtos = FlattenMappings([result]);
|
||||
var response = dtos.FirstOrDefault();
|
||||
|
||||
return CreatedAtAction(nameof(GetByCveIdAsync), new { cveId = result.CveId }, response);
|
||||
}
|
||||
@@ -324,16 +280,16 @@ public class CveMappingController : ControllerBase
|
||||
var response = new PatchAnalysisResponse
|
||||
{
|
||||
CommitUrl = request.CommitUrl,
|
||||
ExtractedSymbols = result.ExtractedSymbols.Select(s => new ExtractedSymbolDto
|
||||
ExtractedSymbols = result.Symbols.Select(s => new ExtractedSymbolDto
|
||||
{
|
||||
Symbol = s.Symbol,
|
||||
FilePath = s.FilePath,
|
||||
StartLine = s.StartLine,
|
||||
EndLine = s.EndLine,
|
||||
ChangeType = s.ChangeType.ToString(),
|
||||
Language = s.Language
|
||||
Symbol = s.Symbol.DisplayName,
|
||||
FilePath = s.SourceFile,
|
||||
StartLine = s.LineRange?.Start,
|
||||
EndLine = s.LineRange?.End,
|
||||
ChangeType = s.Type.ToString(),
|
||||
Language = null
|
||||
}).ToList(),
|
||||
AnalyzedAt = result.AnalyzedAt
|
||||
AnalyzedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
@@ -367,23 +323,13 @@ public class CveMappingController : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
var dtos = FlattenMappings(enrichedMappings);
|
||||
|
||||
var response = new EnrichmentResponse
|
||||
{
|
||||
CveId = cveId,
|
||||
EnrichedCount = enrichedMappings.Count,
|
||||
Mappings = enrichedMappings.Select(m => new CveMappingDto
|
||||
{
|
||||
CveId = m.CveId,
|
||||
Purl = m.Purl,
|
||||
Symbol = m.Symbol.Symbol,
|
||||
CanonicalId = m.Symbol.CanonicalId,
|
||||
FilePath = m.Symbol.FilePath,
|
||||
Source = m.Source.ToString(),
|
||||
Confidence = m.Confidence,
|
||||
VulnerabilityType = m.VulnerabilityType.ToString(),
|
||||
AffectedVersions = m.AffectedVersions.ToList(),
|
||||
FixedVersions = m.FixedVersions.ToList()
|
||||
}).ToList()
|
||||
EnrichedCount = dtos.Count,
|
||||
Mappings = dtos
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
@@ -415,6 +361,31 @@ public class CveMappingController : ControllerBase
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens CveSymbolMappings (which contain multiple Symbols each) into a flat list of CveMappingDto.
|
||||
/// </summary>
|
||||
private static List<CveMappingDto> FlattenMappings(
|
||||
IReadOnlyList<CveSymbolMapping> mappings,
|
||||
string? overrideCveId = null)
|
||||
{
|
||||
return mappings.SelectMany(m => m.Symbols.Select(s => new CveMappingDto
|
||||
{
|
||||
CveId = overrideCveId ?? m.CveId,
|
||||
Purl = s.Symbol.Purl ?? m.AffectedPurls.FirstOrDefault() ?? string.Empty,
|
||||
Symbol = s.Symbol.DisplayName,
|
||||
CanonicalId = s.Symbol.CanonicalId,
|
||||
FilePath = s.SourceFile,
|
||||
StartLine = s.LineRange?.Start,
|
||||
EndLine = s.LineRange?.End,
|
||||
Source = m.Source.ToString(),
|
||||
Confidence = s.Confidence,
|
||||
VulnerabilityType = s.Type.ToString(),
|
||||
AffectedVersions = m.AffectedPurls.ToList(),
|
||||
FixedVersions = null,
|
||||
EvidenceUri = m.PatchCommitUrl
|
||||
})).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -137,7 +137,7 @@ public class ReachabilityController : ControllerBase
|
||||
IncludeStatic = request.IncludeStatic ?? true,
|
||||
IncludeRuntime = request.IncludeRuntime ?? true,
|
||||
ObservationWindow = request.ObservationWindow ?? TimeSpan.FromDays(7),
|
||||
ConfidenceThreshold = request.ConfidenceThreshold ?? 0.8
|
||||
MinConfidenceThreshold = request.MinConfidenceThreshold ?? 0.8
|
||||
};
|
||||
|
||||
var tenantId = GetTenantId();
|
||||
@@ -187,7 +187,7 @@ public class ReachabilityController : ControllerBase
|
||||
IncludeStatic = request.IncludeStatic ?? true,
|
||||
IncludeRuntime = request.IncludeRuntime ?? true,
|
||||
ObservationWindow = request.ObservationWindow ?? TimeSpan.FromDays(7),
|
||||
ConfidenceThreshold = request.ConfidenceThreshold ?? 0.8
|
||||
MinConfidenceThreshold = request.MinConfidenceThreshold ?? 0.8
|
||||
};
|
||||
|
||||
var tenantId = GetTenantId();
|
||||
@@ -223,8 +223,8 @@ public class ReachabilityController : ControllerBase
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrEmpty(symbol.Namespace)) parts.Add(symbol.Namespace);
|
||||
if (!string.IsNullOrEmpty(symbol.TypeName)) parts.Add(symbol.TypeName);
|
||||
if (!string.IsNullOrEmpty(symbol.MemberName)) parts.Add(symbol.MemberName);
|
||||
if (!string.IsNullOrEmpty(symbol.Type)) parts.Add(symbol.Type);
|
||||
if (!string.IsNullOrEmpty(symbol.Method)) parts.Add(symbol.Method);
|
||||
return string.Join(".", parts);
|
||||
}
|
||||
}
|
||||
@@ -277,7 +277,7 @@ public record HybridQueryRequest
|
||||
public TimeSpan? ObservationWindow { get; init; }
|
||||
|
||||
/// <summary>Confidence threshold for verdict. Default: 0.8.</summary>
|
||||
public double? ConfidenceThreshold { get; init; }
|
||||
public double? MinConfidenceThreshold { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -301,7 +301,7 @@ public record BatchQueryRequest
|
||||
public TimeSpan? ObservationWindow { get; init; }
|
||||
|
||||
/// <summary>Confidence threshold for verdict. Default: 0.8.</summary>
|
||||
public double? ConfidenceThreshold { get; init; }
|
||||
public double? MinConfidenceThreshold { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
// Stub types for CVE-Symbol mapping service
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Reachability.Core.CveMapping;
|
||||
|
||||
/// <summary>
|
||||
/// Service for CVE-symbol mapping operations.
|
||||
/// </summary>
|
||||
public interface ICveSymbolMappingService
|
||||
{
|
||||
Task<IReadOnlyList<CveSymbolMapping>> GetMappingsForCveAsync(string cveId, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<CveSymbolMapping>> GetMappingsForPackageAsync(string purl, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<CveSymbolMapping>> SearchBySymbolAsync(string symbol, string? language, CancellationToken cancellationToken);
|
||||
Task<CveSymbolMapping> AddOrUpdateMappingAsync(CveSymbolMapping mapping, CancellationToken cancellationToken);
|
||||
Task<PatchAnalysisResult> AnalyzePatchAsync(string? commitUrl, string? diffContent, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<CveSymbolMapping>> EnrichFromOsvAsync(string cveId, CancellationToken cancellationToken);
|
||||
Task<MappingStats> GetStatsAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mapping between a CVE and a vulnerable symbol.
|
||||
/// </summary>
|
||||
public record CveSymbolMapping
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string Purl { get; init; }
|
||||
public required VulnerableSymbol Symbol { get; init; }
|
||||
public MappingSource Source { get; init; }
|
||||
public double Confidence { get; init; }
|
||||
public VulnerabilityType VulnerabilityType { get; init; }
|
||||
public ImmutableArray<string> AffectedVersions { get; init; } = [];
|
||||
public ImmutableArray<string> FixedVersions { get; init; } = [];
|
||||
public string? EvidenceUri { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a vulnerable symbol (function/method).
|
||||
/// </summary>
|
||||
public record VulnerableSymbol
|
||||
{
|
||||
public required string Symbol { get; init; }
|
||||
public string? CanonicalId { get; init; }
|
||||
public string? FilePath { get; init; }
|
||||
public int? StartLine { get; init; }
|
||||
public int? EndLine { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source of the mapping.
|
||||
/// </summary>
|
||||
public enum MappingSource
|
||||
{
|
||||
Unknown = 0,
|
||||
Osv = 1,
|
||||
Nvd = 2,
|
||||
Manual = 3,
|
||||
PatchAnalysis = 4,
|
||||
Vendor = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of vulnerability.
|
||||
/// </summary>
|
||||
public enum VulnerabilityType
|
||||
{
|
||||
Unknown = 0,
|
||||
BufferOverflow = 1,
|
||||
SqlInjection = 2,
|
||||
XSS = 3,
|
||||
CommandInjection = 4,
|
||||
PathTraversal = 5,
|
||||
Deserialization = 6,
|
||||
Cryptographic = 7,
|
||||
Other = 99
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of patch analysis.
|
||||
/// </summary>
|
||||
public record PatchAnalysisResult
|
||||
{
|
||||
public required IReadOnlyList<ExtractedSymbol> ExtractedSymbols { get; init; }
|
||||
public DateTimeOffset AnalyzedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symbol extracted from a patch.
|
||||
/// </summary>
|
||||
public record ExtractedSymbol
|
||||
{
|
||||
public required string Symbol { get; init; }
|
||||
public string? FilePath { get; init; }
|
||||
public int? StartLine { get; init; }
|
||||
public int? EndLine { get; init; }
|
||||
public ChangeType ChangeType { get; init; }
|
||||
public string? Language { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of change in a patch.
|
||||
/// </summary>
|
||||
public enum ChangeType
|
||||
{
|
||||
Unknown = 0,
|
||||
Added = 1,
|
||||
Modified = 2,
|
||||
Deleted = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistics about the 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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null implementation of the CVE symbol mapping service.
|
||||
/// </summary>
|
||||
public sealed class NullCveSymbolMappingService : ICveSymbolMappingService
|
||||
{
|
||||
public Task<IReadOnlyList<CveSymbolMapping>> GetMappingsForCveAsync(string cveId, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<CveSymbolMapping>>([]);
|
||||
|
||||
public Task<IReadOnlyList<CveSymbolMapping>> GetMappingsForPackageAsync(string purl, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<CveSymbolMapping>>([]);
|
||||
|
||||
public Task<IReadOnlyList<CveSymbolMapping>> SearchBySymbolAsync(string symbol, string? language, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<CveSymbolMapping>>([]);
|
||||
|
||||
public Task<CveSymbolMapping> AddOrUpdateMappingAsync(CveSymbolMapping mapping, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(mapping);
|
||||
|
||||
public Task<PatchAnalysisResult> AnalyzePatchAsync(string? commitUrl, string? diffContent, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new PatchAnalysisResult { ExtractedSymbols = [], AnalyzedAt = DateTimeOffset.UtcNow });
|
||||
|
||||
public Task<IReadOnlyList<CveSymbolMapping>> EnrichFromOsvAsync(string cveId, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<CveSymbolMapping>>([]);
|
||||
|
||||
public Task<MappingStats> GetStatsAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new MappingStats { LastUpdated = DateTimeOffset.UtcNow });
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Reachability.Core;
|
||||
using ExecutionContext = StellaOps.Reachability.Core.ExecutionContext;
|
||||
|
||||
namespace StellaOps.ReachGraph.WebService.Services;
|
||||
|
||||
@@ -62,9 +63,8 @@ public sealed class InMemorySignalsAdapter : ISignalsAdapter
|
||||
.Select(o => new ExecutionContext
|
||||
{
|
||||
Environment = o.Environment ?? "production",
|
||||
Service = o.ServiceName,
|
||||
TraceId = o.TraceId,
|
||||
ObservedAt = o.ObservedAt
|
||||
ContainerId = o.ServiceName,
|
||||
Route = o.TraceId,
|
||||
})
|
||||
.Distinct()
|
||||
.Take(10)
|
||||
@@ -157,7 +157,7 @@ public sealed class InMemorySignalsAdapter : ISignalsAdapter
|
||||
Environment = environment ?? "production",
|
||||
ServiceName = serviceName,
|
||||
TraceId = traceId,
|
||||
EvidenceUri = EvidenceUriBuilder.Build("signals", artifactDigest, $"symbol:{symbolFqn}")
|
||||
EvidenceUri = new EvidenceUriBuilder().BuildRuntimeFactsUri("default", artifactDigest, symbolFqn)
|
||||
};
|
||||
|
||||
var list = _observations.GetOrAdd(key, _ => new List<ObservedSymbol>());
|
||||
@@ -197,8 +197,8 @@ public sealed class InMemorySignalsAdapter : ISignalsAdapter
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrEmpty(symbol.Namespace)) parts.Add(symbol.Namespace);
|
||||
if (!string.IsNullOrEmpty(symbol.TypeName)) parts.Add(symbol.TypeName);
|
||||
if (!string.IsNullOrEmpty(symbol.MemberName)) parts.Add(symbol.MemberName);
|
||||
if (!string.IsNullOrEmpty(symbol.Type)) parts.Add(symbol.Type);
|
||||
if (!string.IsNullOrEmpty(symbol.Method)) parts.Add(symbol.Method);
|
||||
return string.Join(".", parts);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,17 +96,17 @@ public sealed class ReachGraphStoreAdapter : IReachGraphAdapter
|
||||
}
|
||||
|
||||
// Count entrypoints from scope
|
||||
var entrypointCount = graph.Scope.Entrypoints?.Length ?? 0;
|
||||
var entrypointCount = graph.Scope.Entrypoints.Length;
|
||||
|
||||
return new ReachGraphMetadata
|
||||
{
|
||||
ArtifactDigest = artifactDigest,
|
||||
GraphDigest = summary.Digest,
|
||||
CreatedAt = summary.CreatedAt,
|
||||
BuiltAt = summary.CreatedAt,
|
||||
NodeCount = graph.Nodes.Length,
|
||||
EdgeCount = graph.Edges.Length,
|
||||
EntrypointCount = entrypointCount,
|
||||
Version = graph.SchemaVersion
|
||||
AnalyzerVersion = graph.SchemaVersion
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,18 +152,18 @@ public sealed class ReachGraphStoreAdapter : IReachGraphAdapter
|
||||
}
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
if (adjacency.ContainsKey(edge.Source))
|
||||
if (adjacency.ContainsKey(edge.From))
|
||||
{
|
||||
adjacency[edge.Source].Add(edge.Target);
|
||||
adjacency[edge.From].Add(edge.To);
|
||||
}
|
||||
}
|
||||
|
||||
// Get entrypoints from scope
|
||||
var entrypoints = graph.Scope.Entrypoints ?? ImmutableArray<string>.Empty;
|
||||
var entrypoints = graph.Scope.Entrypoints;
|
||||
if (entrypoints.Length == 0)
|
||||
{
|
||||
// If no entrypoints defined, try to find nodes with no incoming edges
|
||||
var hasIncoming = new HashSet<string>(graph.Edges.Select(e => e.Target));
|
||||
var hasIncoming = new HashSet<string>(graph.Edges.Select(e => e.To));
|
||||
entrypoints = graph.Nodes
|
||||
.Where(n => !hasIncoming.Contains(n.Id))
|
||||
.Select(n => n.Id)
|
||||
@@ -215,8 +215,8 @@ public sealed class ReachGraphStoreAdapter : IReachGraphAdapter
|
||||
}
|
||||
|
||||
// Check individual parts
|
||||
if (!string.IsNullOrEmpty(symbol.MemberName) &&
|
||||
node.Ref.Contains(symbol.MemberName, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.IsNullOrEmpty(symbol.Method) &&
|
||||
node.Ref.Contains(symbol.Method, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -228,8 +228,8 @@ public sealed class ReachGraphStoreAdapter : IReachGraphAdapter
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrEmpty(symbol.Namespace)) parts.Add(symbol.Namespace);
|
||||
if (!string.IsNullOrEmpty(symbol.TypeName)) parts.Add(symbol.TypeName);
|
||||
if (!string.IsNullOrEmpty(symbol.MemberName)) parts.Add(symbol.MemberName);
|
||||
if (!string.IsNullOrEmpty(symbol.Type)) parts.Add(symbol.Type);
|
||||
if (!string.IsNullOrEmpty(symbol.Method)) parts.Add(symbol.Method);
|
||||
return string.Join(".", parts);
|
||||
}
|
||||
|
||||
@@ -273,8 +273,8 @@ public sealed class ReachGraphStoreAdapter : IReachGraphAdapter
|
||||
private static ImmutableArray<string> CreateEvidenceUris(ReachGraphMinimal graph, SymbolRef symbol)
|
||||
{
|
||||
var artifactDigest = graph.Artifact.Digest ?? "unknown";
|
||||
var symbolFqn = BuildSymbolFqn(symbol);
|
||||
var evidenceUri = EvidenceUriBuilder.Build("reachgraph", artifactDigest, $"symbol:{symbolFqn}");
|
||||
var builder = new EvidenceUriBuilder();
|
||||
var evidenceUri = builder.BuildReachGraphSliceUri(artifactDigest, symbol.CanonicalId);
|
||||
|
||||
return ImmutableArray.Create(evidenceUri);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.ReachGraph.Persistence\StellaOps.ReachGraph.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.ReachGraph.Cache\StellaOps.ReachGraph.Cache.csproj" />
|
||||
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Reachability.Core\StellaOps.Reachability.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -21,8 +21,9 @@ public class InMemorySignalsAdapterTests
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Namespace = "System",
|
||||
TypeName = "String",
|
||||
MemberName = "Trim"
|
||||
Purl = "pkg:nuget/test@1.0.0",
|
||||
Type ="String",
|
||||
Method ="Trim"
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -47,8 +48,9 @@ public class InMemorySignalsAdapterTests
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Namespace = "MyApp",
|
||||
TypeName = "Service",
|
||||
MemberName = "Process"
|
||||
Purl = "pkg:nuget/test@1.0.0",
|
||||
Type ="Service",
|
||||
Method ="Process"
|
||||
};
|
||||
|
||||
adapter.RecordObservation(
|
||||
@@ -84,8 +86,9 @@ public class InMemorySignalsAdapterTests
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Namespace = "MyApp",
|
||||
TypeName = "Service",
|
||||
MemberName = "Process"
|
||||
Purl = "pkg:nuget/test@1.0.0",
|
||||
Type ="Service",
|
||||
Method ="Process"
|
||||
};
|
||||
|
||||
// Record observation 10 days ago
|
||||
@@ -117,8 +120,9 @@ public class InMemorySignalsAdapterTests
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Namespace = "MyApp",
|
||||
TypeName = "Service",
|
||||
MemberName = "Process"
|
||||
Purl = "pkg:nuget/test@1.0.0",
|
||||
Type ="Service",
|
||||
Method ="Process"
|
||||
};
|
||||
|
||||
adapter.RecordObservation(
|
||||
@@ -157,8 +161,9 @@ public class InMemorySignalsAdapterTests
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Namespace = "MyApp",
|
||||
TypeName = "Service",
|
||||
MemberName = "Process"
|
||||
Purl = "pkg:nuget/test@1.0.0",
|
||||
Type ="Service",
|
||||
Method ="Process"
|
||||
};
|
||||
|
||||
adapter.RecordObservation(
|
||||
@@ -182,8 +187,7 @@ public class InMemorySignalsAdapterTests
|
||||
// Assert
|
||||
result.Contexts.Should().NotBeEmpty();
|
||||
result.Contexts[0].Environment.Should().Be("production");
|
||||
result.Contexts[0].Service.Should().Be("api-gateway");
|
||||
result.Contexts[0].TraceId.Should().Be("trace-001");
|
||||
result.Contexts[0].ContainerId.Should().Be("api-gateway");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -194,8 +198,9 @@ public class InMemorySignalsAdapterTests
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Namespace = "MyApp",
|
||||
TypeName = "Service",
|
||||
MemberName = "Process"
|
||||
Purl = "pkg:nuget/test@1.0.0",
|
||||
Type ="Service",
|
||||
Method ="Process"
|
||||
};
|
||||
|
||||
adapter.RecordObservation(
|
||||
@@ -225,8 +230,9 @@ public class InMemorySignalsAdapterTests
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Namespace = "MyApp",
|
||||
TypeName = "Service",
|
||||
MemberName = "Process"
|
||||
Purl = "pkg:nuget/test@1.0.0",
|
||||
Type ="Service",
|
||||
Method ="Process"
|
||||
};
|
||||
|
||||
adapter.RecordObservation(
|
||||
@@ -264,8 +270,9 @@ public class InMemorySignalsAdapterTests
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Namespace = "MyApp",
|
||||
TypeName = "Service",
|
||||
MemberName = "Process"
|
||||
Purl = "pkg:nuget/test@1.0.0",
|
||||
Type ="Service",
|
||||
Method ="Process"
|
||||
};
|
||||
|
||||
adapter.RecordObservation(
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Reachability.Core;
|
||||
using StellaOps.ReachGraph.Persistence;
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using StellaOps.ReachGraph.WebService.Services;
|
||||
using Xunit;
|
||||
@@ -23,9 +23,10 @@ public class ReachGraphStoreAdapterTests
|
||||
var adapter = CreateAdapter();
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Purl = "pkg:nuget/System@1.0.0",
|
||||
Namespace = "System",
|
||||
TypeName = "String",
|
||||
MemberName = "Trim"
|
||||
Type = "String",
|
||||
Method = "Trim"
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -48,9 +49,10 @@ public class ReachGraphStoreAdapterTests
|
||||
var adapter = CreateAdapter();
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Purl = "pkg:nuget/MyApp@1.0.0",
|
||||
Namespace = "MyApp",
|
||||
TypeName = "VulnerableClass",
|
||||
MemberName = "Execute"
|
||||
Type = "VulnerableClass",
|
||||
Method = "Execute"
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -59,7 +61,7 @@ public class ReachGraphStoreAdapterTests
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.IsReachable.Should().BeTrue();
|
||||
result.DistanceFromEntrypoint.Should().BeGreaterThanOrEqualTo(0);
|
||||
result.ShortestPathLength.Should().BeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -72,9 +74,10 @@ public class ReachGraphStoreAdapterTests
|
||||
var adapter = CreateAdapter();
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Purl = "pkg:nuget/NonExistent@1.0.0",
|
||||
Namespace = "NonExistent",
|
||||
TypeName = "Class",
|
||||
MemberName = "Method"
|
||||
Type = "Class",
|
||||
Method = "Method"
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -151,7 +154,7 @@ public class ReachGraphStoreAdapterTests
|
||||
return new ReachGraphStoreAdapter(
|
||||
_storeService,
|
||||
_timeProvider,
|
||||
NullLogger<ReachGraphStoreAdapter>.Instance);
|
||||
"test-tenant");
|
||||
}
|
||||
|
||||
private static ReachGraphMinimal CreateTestGraph(string artifactDigest)
|
||||
@@ -160,53 +163,63 @@ public class ReachGraphStoreAdapterTests
|
||||
{
|
||||
Id = "entry-main",
|
||||
Ref = "MyApp.Program.Main",
|
||||
Kind = "method",
|
||||
Depth = 0
|
||||
Kind = ReachGraphNodeKind.Function,
|
||||
IsEntrypoint = true
|
||||
};
|
||||
|
||||
var vulnerableClass = new ReachGraphNode
|
||||
{
|
||||
Id = "vulnerable-class",
|
||||
Ref = "MyApp.VulnerableClass.Execute",
|
||||
Kind = "method",
|
||||
Depth = 1
|
||||
Kind = ReachGraphNodeKind.Function,
|
||||
IsSink = true
|
||||
};
|
||||
|
||||
var otherNode = new ReachGraphNode
|
||||
{
|
||||
Id = "other-node",
|
||||
Ref = "MyApp.OtherClass.DoWork",
|
||||
Kind = "method",
|
||||
Depth = 2
|
||||
Kind = ReachGraphNodeKind.Function
|
||||
};
|
||||
|
||||
var edges = ImmutableArray.Create(
|
||||
new ReachGraphEdge
|
||||
{
|
||||
Source = "entry-main",
|
||||
Target = "vulnerable-class"
|
||||
From = "entry-main",
|
||||
To = "vulnerable-class",
|
||||
Why = new EdgeExplanation
|
||||
{
|
||||
Type = EdgeExplanationType.DirectCall,
|
||||
Confidence = 1.0
|
||||
}
|
||||
},
|
||||
new ReachGraphEdge
|
||||
{
|
||||
Source = "entry-main",
|
||||
Target = "other-node"
|
||||
From = "entry-main",
|
||||
To = "other-node",
|
||||
Why = new EdgeExplanation
|
||||
{
|
||||
Type = EdgeExplanationType.DirectCall,
|
||||
Confidence = 1.0
|
||||
}
|
||||
});
|
||||
|
||||
return new ReachGraphMinimal
|
||||
{
|
||||
Artifact = new ReachGraphArtifact
|
||||
Artifact = new ReachGraphArtifact(
|
||||
"test-artifact",
|
||||
artifactDigest,
|
||||
ImmutableArray.Create("test")),
|
||||
Scope = new ReachGraphScope(
|
||||
ImmutableArray.Create("entry-main"),
|
||||
ImmutableArray<string>.Empty,
|
||||
null),
|
||||
Provenance = new ReachGraphProvenance
|
||||
{
|
||||
Name = "test-artifact",
|
||||
Digest = artifactDigest,
|
||||
Env = "test"
|
||||
Inputs = new ReachGraphInputs { Sbom = "sha256:sbom-test" },
|
||||
ComputedAt = DateTimeOffset.UtcNow,
|
||||
Analyzer = new ReachGraphAnalyzer("test-analyzer", "1.0.0", "sha256:toolchain")
|
||||
},
|
||||
Scope = new ReachGraphScope
|
||||
{
|
||||
Entrypoints = ImmutableArray.Create("entry-main"),
|
||||
Selectors = ImmutableArray<string>.Empty,
|
||||
Cves = null
|
||||
},
|
||||
Signature = null,
|
||||
Nodes = ImmutableArray.Create(entrypoint, vulnerableClass, otherNode),
|
||||
Edges = edges
|
||||
};
|
||||
@@ -220,16 +233,16 @@ internal sealed class InMemoryReachGraphStoreService : IReachGraphStoreService
|
||||
{
|
||||
private readonly Dictionary<string, ReachGraphMinimal> _graphs = new();
|
||||
|
||||
public Task<ReachGraphStoreResult> UpsertAsync(
|
||||
public Task<StoreResult> UpsertAsync(
|
||||
ReachGraphMinimal graph,
|
||||
string? tenantId,
|
||||
CancellationToken ct)
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var digest = graph.Artifact.Digest;
|
||||
var created = !_graphs.ContainsKey(digest);
|
||||
_graphs[digest] = graph;
|
||||
|
||||
return Task.FromResult(new ReachGraphStoreResult
|
||||
return Task.FromResult(new StoreResult
|
||||
{
|
||||
Digest = digest,
|
||||
ArtifactDigest = digest,
|
||||
@@ -242,28 +255,40 @@ internal sealed class InMemoryReachGraphStoreService : IReachGraphStoreService
|
||||
|
||||
public Task<ReachGraphMinimal?> GetByDigestAsync(
|
||||
string digest,
|
||||
string? tenantId,
|
||||
CancellationToken ct)
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_graphs.TryGetValue(digest, out var graph);
|
||||
return Task.FromResult(graph);
|
||||
}
|
||||
|
||||
public Task<ReachGraphMinimal?> GetByArtifactAsync(
|
||||
public Task<IReadOnlyList<ReachGraphSummary>> ListByArtifactAsync(
|
||||
string artifactDigest,
|
||||
string? tenantId,
|
||||
CancellationToken ct)
|
||||
string tenantId,
|
||||
int limit = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var graph = _graphs.Values.FirstOrDefault(g => g.Artifact.Digest == artifactDigest);
|
||||
return Task.FromResult(graph);
|
||||
var summaries = _graphs.Values
|
||||
.Where(g => g.Artifact.Digest == artifactDigest)
|
||||
.Select(g => new ReachGraphSummary
|
||||
{
|
||||
Digest = g.Artifact.Digest,
|
||||
ArtifactDigest = g.Artifact.Digest,
|
||||
NodeCount = g.Nodes.Length,
|
||||
EdgeCount = g.Edges.Length,
|
||||
BlobSizeBytes = 0,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Scope = g.Scope
|
||||
})
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<ReachGraphSummary>>(summaries);
|
||||
}
|
||||
|
||||
public Task<bool> ExistsAsync(string digest, string? tenantId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(_graphs.ContainsKey(digest));
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(string digest, string? tenantId, CancellationToken ct)
|
||||
public Task<bool> DeleteAsync(
|
||||
string digest,
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(_graphs.Remove(digest));
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.ReachGraph.WebService\StellaOps.ReachGraph.WebService.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user