Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# AGENTS - Scanner VulnSurfaces Library
|
||||
|
||||
## Mission
|
||||
Build and serve vulnerability surface data for CVE and package-level symbol mapping.
|
||||
|
||||
## Roles
|
||||
- Backend engineer (.NET 10, C# preview).
|
||||
- QA engineer (unit tests with deterministic fixtures).
|
||||
|
||||
## Required Reading
|
||||
- `docs/README.md`
|
||||
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/modules/scanner/architecture.md`
|
||||
- `docs/reachability/slice-schema.md`
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: `src/Scanner/__Libraries/StellaOps.Scanner.VulnSurfaces/`
|
||||
- Tests: `src/Scanner/__Tests/StellaOps.Scanner.VulnSurfaces.Tests/`
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint.
|
||||
|
||||
## Determinism & Offline Rules
|
||||
- Deterministic ordering for symbol lists and surface results.
|
||||
- Offline-first: no network in tests; use fixtures and mock clients.
|
||||
|
||||
## Testing Expectations
|
||||
- Unit tests for CVE to symbol mapping decisions and fallbacks.
|
||||
- Deterministic results for repeated inputs.
|
||||
|
||||
## Workflow
|
||||
- Update sprint status on task transitions.
|
||||
- Record notable decisions in the sprint Execution Log.
|
||||
@@ -16,6 +16,12 @@ namespace StellaOps.Scanner.VulnSurfaces.Models;
|
||||
/// </summary>
|
||||
public sealed record VulnSurface
|
||||
{
|
||||
/// <summary>
|
||||
/// Database UUID for storage lookups.
|
||||
/// </summary>
|
||||
[JsonPropertyName("surface_guid")]
|
||||
public Guid? SurfaceGuid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Database ID.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.VulnSurfaces.Services;
|
||||
|
||||
public interface IPackageSymbolProvider
|
||||
{
|
||||
Task<ImmutableArray<AffectedSymbol>> GetPublicSymbolsAsync(string purl, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class NullPackageSymbolProvider : IPackageSymbolProvider
|
||||
{
|
||||
public Task<ImmutableArray<AffectedSymbol>> GetPublicSymbolsAsync(string purl, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(ImmutableArray<AffectedSymbol>.Empty);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.VulnSurfaces.Services;
|
||||
|
||||
public interface IVulnSurfaceService
|
||||
{
|
||||
Task<VulnSurfaceResult> GetAffectedSymbolsAsync(
|
||||
string cveId,
|
||||
string purl,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record VulnSurfaceResult
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string Purl { get; init; }
|
||||
public required ImmutableArray<AffectedSymbol> Symbols { get; init; }
|
||||
public required string Source { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AffectedSymbol
|
||||
{
|
||||
public required string SymbolId { get; init; }
|
||||
public string? MethodKey { get; init; }
|
||||
public string? DisplayName { get; init; }
|
||||
public string? ChangeType { get; init; }
|
||||
public double Confidence { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.VulnSurfaces.Models;
|
||||
using StellaOps.Scanner.VulnSurfaces.Storage;
|
||||
|
||||
namespace StellaOps.Scanner.VulnSurfaces.Services;
|
||||
|
||||
public sealed class VulnSurfaceService : IVulnSurfaceService
|
||||
{
|
||||
private readonly IVulnSurfaceRepository _repository;
|
||||
private readonly IPackageSymbolProvider _packageSymbolProvider;
|
||||
|
||||
public VulnSurfaceService(
|
||||
IVulnSurfaceRepository repository,
|
||||
IPackageSymbolProvider? packageSymbolProvider = null)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_packageSymbolProvider = packageSymbolProvider ?? new NullPackageSymbolProvider();
|
||||
}
|
||||
|
||||
public async Task<VulnSurfaceResult> GetAffectedSymbolsAsync(
|
||||
string cveId,
|
||||
string purl,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cveId))
|
||||
{
|
||||
throw new ArgumentException("CVE ID is required.", nameof(cveId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(purl))
|
||||
{
|
||||
throw new ArgumentException("PURL is required.", nameof(purl));
|
||||
}
|
||||
|
||||
var normalizedCve = cveId.Trim().ToUpperInvariant();
|
||||
var normalizedPurl = purl.Trim();
|
||||
|
||||
var parsed = PurlParts.TryParse(normalizedPurl);
|
||||
VulnSurface? surface = null;
|
||||
if (parsed is not null && parsed.Value.Version is not null)
|
||||
{
|
||||
surface = await _repository.GetByCveAndPackageAsync(
|
||||
parsed.Value.TenantId,
|
||||
normalizedCve,
|
||||
parsed.Value.Ecosystem,
|
||||
parsed.Value.Name,
|
||||
parsed.Value.Version,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
IReadOnlyList<VulnSurfaceSink> sinks = Array.Empty<VulnSurfaceSink>();
|
||||
if (surface?.SurfaceGuid is { } surfaceGuid && surfaceGuid != Guid.Empty)
|
||||
{
|
||||
sinks = await _repository.GetSinksAsync(surfaceGuid, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (sinks.Count == 0)
|
||||
{
|
||||
var fallbackSymbols = await _packageSymbolProvider.GetPublicSymbolsAsync(normalizedPurl, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new VulnSurfaceResult
|
||||
{
|
||||
CveId = normalizedCve,
|
||||
Purl = normalizedPurl,
|
||||
Symbols = fallbackSymbols,
|
||||
Source = fallbackSymbols.IsEmpty ? "heuristic" : "package-symbols",
|
||||
Confidence = fallbackSymbols.IsEmpty ? 0.2 : 0.4
|
||||
};
|
||||
}
|
||||
|
||||
var symbolList = sinks
|
||||
.Select(sink => new AffectedSymbol
|
||||
{
|
||||
SymbolId = sink.MethodKey,
|
||||
MethodKey = sink.MethodKey,
|
||||
DisplayName = $"{sink.DeclaringType}.{sink.MethodName}",
|
||||
ChangeType = sink.ChangeType.ToString(),
|
||||
Confidence = surface?.Confidence ?? 0.8
|
||||
})
|
||||
.OrderBy(s => s.SymbolId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return new VulnSurfaceResult
|
||||
{
|
||||
CveId = normalizedCve,
|
||||
Purl = normalizedPurl,
|
||||
Symbols = symbolList,
|
||||
Source = "surface",
|
||||
Confidence = surface?.Confidence ?? 0.8
|
||||
};
|
||||
}
|
||||
|
||||
private readonly record struct PurlParts(
|
||||
string Ecosystem,
|
||||
string Name,
|
||||
string? Version,
|
||||
Guid TenantId)
|
||||
{
|
||||
public static PurlParts? TryParse(string purl)
|
||||
{
|
||||
if (!purl.StartsWith("pkg:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var withoutPrefix = purl.Substring(4);
|
||||
var slashIndex = withoutPrefix.IndexOf('/');
|
||||
if (slashIndex <= 0 || slashIndex == withoutPrefix.Length - 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ecosystem = withoutPrefix[..slashIndex];
|
||||
var remainder = withoutPrefix[(slashIndex + 1)..];
|
||||
var versionIndex = remainder.IndexOf('@');
|
||||
string? version = null;
|
||||
var namePart = remainder;
|
||||
if (versionIndex > 0)
|
||||
{
|
||||
namePart = remainder[..versionIndex];
|
||||
version = versionIndex < remainder.Length - 1 ? remainder[(versionIndex + 1)..] : null;
|
||||
}
|
||||
|
||||
var name = Uri.UnescapeDataString(namePart);
|
||||
return new PurlParts(
|
||||
ecosystem.Trim().ToLowerInvariant(),
|
||||
name.Trim(),
|
||||
string.IsNullOrWhiteSpace(version) ? null : version.Trim(),
|
||||
TenantId: Guid.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,9 +348,12 @@ public sealed class PostgresVulnSurfaceRepository : IVulnSurfaceRepository
|
||||
|
||||
private static VulnSurface MapToVulnSurface(NpgsqlDataReader reader)
|
||||
{
|
||||
var surfaceGuid = reader.GetGuid(0);
|
||||
|
||||
return new VulnSurface
|
||||
{
|
||||
SurfaceId = reader.GetGuid(0).GetHashCode(),
|
||||
SurfaceGuid = surfaceGuid,
|
||||
SurfaceId = surfaceGuid.GetHashCode(),
|
||||
CveId = reader.GetString(2),
|
||||
PackageId = $"pkg:{reader.GetString(3)}/{reader.GetString(4)}@{reader.GetString(5)}",
|
||||
Ecosystem = reader.GetString(3),
|
||||
|
||||
Reference in New Issue
Block a user