- Fix namespace conflicts (Subgraph → PoESubgraph) - Add hash sanitization for Windows filesystem (colon → underscore) - Update all test mocks to use It.IsAny<>() - Add direct orchestrator unit tests - All 8 PoE tests now passing (100% success rate) - Complete SPRINT_3500_0001_0001 documentation Fixes compilation errors and Windows filesystem compatibility issues. Tests: 8/8 passing Files: 8 modified, 1 new test, 1 completion report 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
209 lines
7.2 KiB
C#
209 lines
7.2 KiB
C#
namespace StellaOps.Concelier.ProofService.Postgres;
|
|
|
|
using Dapper;
|
|
using Microsoft.Extensions.Logging;
|
|
using Npgsql;
|
|
using StellaOps.Concelier.ProofService;
|
|
using StellaOps.Feedser.BinaryAnalysis.Models;
|
|
|
|
/// <summary>
|
|
/// PostgreSQL implementation of patch repository.
|
|
/// Queries vuln.patch_evidence and feedser.binary_fingerprints tables.
|
|
/// </summary>
|
|
public sealed class PostgresPatchRepository : IPatchRepository
|
|
{
|
|
private readonly string _connectionString;
|
|
private readonly ILogger<PostgresPatchRepository> _logger;
|
|
|
|
public PostgresPatchRepository(
|
|
string connectionString,
|
|
ILogger<PostgresPatchRepository> logger)
|
|
{
|
|
_connectionString = connectionString;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find patch headers mentioning the given CVE ID.
|
|
/// Returns all matching patch headers ordered by parsed date (newest first).
|
|
/// </summary>
|
|
public async Task<IReadOnlyList<PatchHeaderDto>> FindPatchHeadersByCveAsync(
|
|
string cveId,
|
|
CancellationToken ct)
|
|
{
|
|
const string sql = @"
|
|
SELECT
|
|
patch_file_path AS PatchFilePath,
|
|
origin AS Origin,
|
|
parsed_at AS ParsedAt,
|
|
cve_ids AS CveIds
|
|
FROM vuln.patch_evidence
|
|
WHERE @CveId = ANY(cve_ids)
|
|
ORDER BY parsed_at DESC;
|
|
";
|
|
|
|
try
|
|
{
|
|
await using var connection = new NpgsqlConnection(_connectionString);
|
|
await connection.OpenAsync(ct);
|
|
|
|
var results = await connection.QueryAsync<PatchHeaderDto>(
|
|
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
|
|
|
|
var patchList = results.ToList();
|
|
|
|
_logger.LogDebug(
|
|
"Found {Count} patch headers for {CveId}",
|
|
patchList.Count, cveId);
|
|
|
|
return patchList;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex,
|
|
"Failed to query patch headers for {CveId}",
|
|
cveId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find patch signatures (HunkSig matches) for the given CVE ID.
|
|
/// Returns all matching signatures ordered by extraction date (newest first).
|
|
/// </summary>
|
|
public async Task<IReadOnlyList<PatchSigDto>> FindPatchSignaturesByCveAsync(
|
|
string cveId,
|
|
CancellationToken ct)
|
|
{
|
|
const string sql = @"
|
|
SELECT
|
|
commit_sha AS CommitSha,
|
|
upstream_repo AS UpstreamRepo,
|
|
extracted_at AS ExtractedAt,
|
|
hunk_hash AS HunkHash
|
|
FROM vuln.patch_signatures
|
|
WHERE cve_id = @CveId
|
|
ORDER BY extracted_at DESC;
|
|
";
|
|
|
|
try
|
|
{
|
|
await using var connection = new NpgsqlConnection(_connectionString);
|
|
await connection.OpenAsync(ct);
|
|
|
|
var results = await connection.QueryAsync<PatchSigDto>(
|
|
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
|
|
|
|
var sigList = results.ToList();
|
|
|
|
_logger.LogDebug(
|
|
"Found {Count} patch signatures for {CveId}",
|
|
sigList.Count, cveId);
|
|
|
|
return sigList;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex,
|
|
"Failed to query patch signatures for {CveId}",
|
|
cveId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find binary fingerprints for the given CVE ID.
|
|
/// Returns all matching fingerprints ordered by extraction date (newest first).
|
|
/// </summary>
|
|
public async Task<IReadOnlyList<BinaryFingerprint>> FindBinaryFingerprintsByCveAsync(
|
|
string cveId,
|
|
CancellationToken ct)
|
|
{
|
|
const string sql = @"
|
|
SELECT
|
|
fingerprint_id AS FingerprintId,
|
|
cve_id AS CveId,
|
|
method AS Method,
|
|
fingerprint_value AS FingerprintValue,
|
|
target_binary AS TargetBinary,
|
|
target_function AS TargetFunction,
|
|
architecture AS Architecture,
|
|
format AS Format,
|
|
compiler AS Compiler,
|
|
optimization_level AS OptimizationLevel,
|
|
has_debug_symbols AS HasDebugSymbols,
|
|
file_offset AS FileOffset,
|
|
region_size AS RegionSize,
|
|
extracted_at AS ExtractedAt,
|
|
extractor_version AS ExtractorVersion
|
|
FROM feedser.binary_fingerprints
|
|
WHERE cve_id = @CveId
|
|
ORDER BY extracted_at DESC;
|
|
";
|
|
|
|
try
|
|
{
|
|
await using var connection = new NpgsqlConnection(_connectionString);
|
|
await connection.OpenAsync(ct);
|
|
|
|
var results = await connection.QueryAsync<BinaryFingerprintRow>(
|
|
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
|
|
|
|
var fingerprints = results.Select(row => new BinaryFingerprint
|
|
{
|
|
FingerprintId = row.FingerprintId,
|
|
CveId = row.CveId,
|
|
Method = Enum.Parse<FingerprintMethod>(row.Method, ignoreCase: true),
|
|
FingerprintValue = row.FingerprintValue,
|
|
TargetBinary = row.TargetBinary,
|
|
TargetFunction = row.TargetFunction,
|
|
Metadata = new FingerprintMetadata
|
|
{
|
|
Architecture = row.Architecture,
|
|
Format = row.Format,
|
|
Compiler = row.Compiler,
|
|
OptimizationLevel = row.OptimizationLevel,
|
|
HasDebugSymbols = row.HasDebugSymbols,
|
|
FileOffset = row.FileOffset,
|
|
RegionSize = row.RegionSize
|
|
},
|
|
ExtractedAt = row.ExtractedAt,
|
|
ExtractorVersion = row.ExtractorVersion
|
|
}).ToList();
|
|
|
|
_logger.LogDebug(
|
|
"Found {Count} binary fingerprints for {CveId}",
|
|
fingerprints.Count, cveId);
|
|
|
|
return fingerprints;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex,
|
|
"Failed to query binary fingerprints for {CveId}",
|
|
cveId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Internal row mapping class for Dapper
|
|
private sealed class BinaryFingerprintRow
|
|
{
|
|
public required string FingerprintId { get; init; }
|
|
public required string CveId { get; init; }
|
|
public required string Method { get; init; }
|
|
public required string FingerprintValue { get; init; }
|
|
public required string TargetBinary { get; init; }
|
|
public string? TargetFunction { get; init; }
|
|
public required string Architecture { get; init; }
|
|
public required string Format { get; init; }
|
|
public string? Compiler { get; init; }
|
|
public string? OptimizationLevel { get; init; }
|
|
public required bool HasDebugSymbols { get; init; }
|
|
public long? FileOffset { get; init; }
|
|
public long? RegionSize { get; init; }
|
|
public required DateTimeOffset ExtractedAt { get; init; }
|
|
public required string ExtractorVersion { get; init; }
|
|
}
|
|
}
|