Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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