save checkpoint

This commit is contained in:
master
2026-02-11 01:32:14 +02:00
parent 5593212b41
commit cf5b72974f
2316 changed files with 68799 additions and 3808 deletions

View File

@@ -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();
}
}
// ============================================================================

View File

@@ -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>

View File

@@ -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 });
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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(

View File

@@ -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));
}

View File

@@ -22,5 +22,6 @@
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.ReachGraph.WebService\StellaOps.ReachGraph.WebService.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>