feat: Add tests for RichGraphPublisher and RichGraphWriter
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
- Implement unit tests for RichGraphPublisher to verify graph publishing to CAS. - Implement unit tests for RichGraphWriter to ensure correct writing of canonical graphs and metadata. feat: Implement AOC Guard validation logic - Add AOC Guard validation logic to enforce document structure and field constraints. - Introduce violation codes for various validation errors. - Implement tests for AOC Guard to validate expected behavior. feat: Create Console Status API client and service - Implement ConsoleStatusClient for fetching console status and streaming run events. - Create ConsoleStatusService to manage console status polling and event subscriptions. - Add tests for ConsoleStatusClient to verify API interactions. feat: Develop Console Status component - Create ConsoleStatusComponent for displaying console status and run events. - Implement UI for showing status metrics and handling user interactions. - Add styles for console status display. test: Add tests for Console Status store - Implement tests for ConsoleStatusStore to verify event handling and state management.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an affected package entry for an advisory.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryAffectedEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid AdvisoryId { get; init; }
|
||||
public required string Ecosystem { get; init; }
|
||||
public required string PackageName { get; init; }
|
||||
public string? Purl { get; init; }
|
||||
public string VersionRange { get; init; } = "{}";
|
||||
public string[]? VersionsAffected { get; init; }
|
||||
public string[]? VersionsFixed { get; init; }
|
||||
public string? DatabaseSpecific { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an advisory alias (e.g., CVE, GHSA).
|
||||
/// </summary>
|
||||
public sealed class AdvisoryAliasEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid AdvisoryId { get; init; }
|
||||
public required string AliasType { get; init; }
|
||||
public required string AliasValue { get; init; }
|
||||
public bool IsPrimary { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a credit entry for an advisory.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryCreditEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid AdvisoryId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? Contact { get; init; }
|
||||
public string? CreditType { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a CVSS score for an advisory.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryCvssEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid AdvisoryId { get; init; }
|
||||
public required string CvssVersion { get; init; }
|
||||
public required string VectorString { get; init; }
|
||||
public decimal BaseScore { get; init; }
|
||||
public string? BaseSeverity { get; init; }
|
||||
public decimal? ExploitabilityScore { get; init; }
|
||||
public decimal? ImpactScore { get; init; }
|
||||
public string? Source { get; init; }
|
||||
public bool IsPrimary { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an advisory reference URL.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryReferenceEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid AdvisoryId { get; init; }
|
||||
public required string RefType { get; init; }
|
||||
public required string Url { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a snapshot of an advisory at a point in time.
|
||||
/// </summary>
|
||||
public sealed class AdvisorySnapshotEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid FeedSnapshotId { get; init; }
|
||||
public required string AdvisoryKey { get; init; }
|
||||
public required string ContentHash { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a CWE weakness linked to an advisory.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryWeaknessEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid AdvisoryId { get; init; }
|
||||
public required string CweId { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? Source { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a feed snapshot record.
|
||||
/// </summary>
|
||||
public sealed class FeedSnapshotEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid SourceId { get; init; }
|
||||
public required string SnapshotId { get; init; }
|
||||
public int AdvisoryCount { get; init; }
|
||||
public string? Checksum { get; init; }
|
||||
public string Metadata { get; init; } = "{}";
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Known Exploited Vulnerability flag entry.
|
||||
/// </summary>
|
||||
public sealed class KevFlagEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid AdvisoryId { get; init; }
|
||||
public required string CveId { get; init; }
|
||||
public string? VendorProject { get; init; }
|
||||
public string? Product { get; init; }
|
||||
public string? VulnerabilityName { get; init; }
|
||||
public DateOnly DateAdded { get; init; }
|
||||
public DateOnly? DueDate { get; init; }
|
||||
public bool KnownRansomwareUse { get; init; }
|
||||
public string? Notes { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a merge event audit record.
|
||||
/// </summary>
|
||||
public sealed class MergeEventEntity
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public Guid AdvisoryId { get; init; }
|
||||
public Guid? SourceId { get; init; }
|
||||
public required string EventType { get; init; }
|
||||
public string? OldValue { get; init; }
|
||||
public string? NewValue { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks source ingestion cursors and metrics.
|
||||
/// </summary>
|
||||
public sealed class SourceStateEntity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid SourceId { get; init; }
|
||||
public string? Cursor { get; init; }
|
||||
public DateTimeOffset? LastSyncAt { get; init; }
|
||||
public DateTimeOffset? LastSuccessAt { get; init; }
|
||||
public string? LastError { get; init; }
|
||||
public long SyncCount { get; init; }
|
||||
public int ErrorCount { get; init; }
|
||||
public string Metadata { get; init; } = "{}";
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for advisory affected packages.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryAffectedRepository : RepositoryBase<ConcelierDataSource>, IAdvisoryAffectedRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public AdvisoryAffectedRepository(ConcelierDataSource dataSource, ILogger<AdvisoryAffectedRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryAffectedEntity> affected, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_affected WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = CreateCommand(deleteSql, connection))
|
||||
{
|
||||
deleteCmd.Transaction = transaction;
|
||||
AddParameter(deleteCmd, "advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_affected
|
||||
(id, advisory_id, ecosystem, package_name, purl, version_range, versions_affected,
|
||||
versions_fixed, database_specific)
|
||||
VALUES
|
||||
(@id, @advisory_id, @ecosystem, @package_name, @purl, @version_range::jsonb,
|
||||
@versions_affected, @versions_fixed, @database_specific::jsonb)
|
||||
""";
|
||||
|
||||
foreach (var entry in affected)
|
||||
{
|
||||
await using var insertCmd = CreateCommand(insertSql, connection);
|
||||
insertCmd.Transaction = transaction;
|
||||
AddParameter(insertCmd, "id", entry.Id);
|
||||
AddParameter(insertCmd, "advisory_id", advisoryId);
|
||||
AddParameter(insertCmd, "ecosystem", entry.Ecosystem);
|
||||
AddParameter(insertCmd, "package_name", entry.PackageName);
|
||||
AddParameter(insertCmd, "purl", entry.Purl);
|
||||
AddJsonbParameter(insertCmd, "version_range", entry.VersionRange);
|
||||
AddTextArrayParameter(insertCmd, "versions_affected", entry.VersionsAffected);
|
||||
AddTextArrayParameter(insertCmd, "versions_fixed", entry.VersionsFixed);
|
||||
AddJsonbParameter(insertCmd, "database_specific", entry.DatabaseSpecific);
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryAffectedEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, ecosystem, package_name, purl, version_range::text,
|
||||
versions_affected, versions_fixed, database_specific::text, created_at
|
||||
FROM vuln.advisory_affected
|
||||
WHERE advisory_id = @advisory_id
|
||||
ORDER BY ecosystem, package_name, purl NULLS LAST
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "advisory_id", advisoryId),
|
||||
MapAffected,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryAffectedEntity>> GetByPurlAsync(string purl, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, ecosystem, package_name, purl, version_range::text,
|
||||
versions_affected, versions_fixed, database_specific::text, created_at
|
||||
FROM vuln.advisory_affected
|
||||
WHERE purl = @purl
|
||||
ORDER BY advisory_id, id
|
||||
LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "purl", purl);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
},
|
||||
MapAffected,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryAffectedEntity>> GetByPackageNameAsync(string ecosystem, string packageName, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, ecosystem, package_name, purl, version_range::text,
|
||||
versions_affected, versions_fixed, database_specific::text, created_at
|
||||
FROM vuln.advisory_affected
|
||||
WHERE ecosystem = @ecosystem AND package_name = @package_name
|
||||
ORDER BY advisory_id, id
|
||||
LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "ecosystem", ecosystem);
|
||||
AddParameter(cmd, "package_name", packageName);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
},
|
||||
MapAffected,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static AdvisoryAffectedEntity MapAffected(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
AdvisoryId = reader.GetGuid(1),
|
||||
Ecosystem = reader.GetString(2),
|
||||
PackageName = reader.GetString(3),
|
||||
Purl = GetNullableString(reader, 4),
|
||||
VersionRange = reader.GetString(5),
|
||||
VersionsAffected = reader.IsDBNull(6) ? null : reader.GetFieldValue<string[]>(6),
|
||||
VersionsFixed = reader.IsDBNull(7) ? null : reader.GetFieldValue<string[]>(7),
|
||||
DatabaseSpecific = GetNullableString(reader, 8),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(9)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for advisory aliases.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryAliasRepository : RepositoryBase<ConcelierDataSource>, IAdvisoryAliasRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public AdvisoryAliasRepository(ConcelierDataSource dataSource, ILogger<AdvisoryAliasRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryAliasEntity> aliases, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_aliases WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = CreateCommand(deleteSql, connection))
|
||||
{
|
||||
deleteCmd.Transaction = transaction;
|
||||
AddParameter(deleteCmd, "advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_aliases
|
||||
(id, advisory_id, alias_type, alias_value, is_primary)
|
||||
VALUES
|
||||
(@id, @advisory_id, @alias_type, @alias_value, @is_primary)
|
||||
""";
|
||||
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
await using var insertCmd = CreateCommand(insertSql, connection);
|
||||
insertCmd.Transaction = transaction;
|
||||
AddParameter(insertCmd, "id", alias.Id);
|
||||
AddParameter(insertCmd, "advisory_id", advisoryId);
|
||||
AddParameter(insertCmd, "alias_type", alias.AliasType);
|
||||
AddParameter(insertCmd, "alias_value", alias.AliasValue);
|
||||
AddParameter(insertCmd, "is_primary", alias.IsPrimary);
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryAliasEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, alias_type, alias_value, is_primary, created_at
|
||||
FROM vuln.advisory_aliases
|
||||
WHERE advisory_id = @advisory_id
|
||||
ORDER BY is_primary DESC, alias_type, alias_value
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "advisory_id", advisoryId),
|
||||
MapAlias,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryAliasEntity>> GetByAliasAsync(string aliasValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, alias_type, alias_value, is_primary, created_at
|
||||
FROM vuln.advisory_aliases
|
||||
WHERE alias_value = @alias_value
|
||||
ORDER BY is_primary DESC
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "alias_value", aliasValue),
|
||||
MapAlias,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static AdvisoryAliasEntity MapAlias(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
AdvisoryId = reader.GetGuid(1),
|
||||
AliasType = reader.GetString(2),
|
||||
AliasValue = reader.GetString(3),
|
||||
IsPrimary = reader.GetBoolean(4),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(5)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for advisory credits.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryCreditRepository : RepositoryBase<ConcelierDataSource>, IAdvisoryCreditRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public AdvisoryCreditRepository(ConcelierDataSource dataSource, ILogger<AdvisoryCreditRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryCreditEntity> credits, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_credits WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = CreateCommand(deleteSql, connection))
|
||||
{
|
||||
deleteCmd.Transaction = transaction;
|
||||
AddParameter(deleteCmd, "advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_credits
|
||||
(id, advisory_id, name, contact, credit_type)
|
||||
VALUES
|
||||
(@id, @advisory_id, @name, @contact, @credit_type)
|
||||
""";
|
||||
|
||||
foreach (var credit in credits)
|
||||
{
|
||||
await using var insertCmd = CreateCommand(insertSql, connection);
|
||||
insertCmd.Transaction = transaction;
|
||||
AddParameter(insertCmd, "id", credit.Id);
|
||||
AddParameter(insertCmd, "advisory_id", advisoryId);
|
||||
AddParameter(insertCmd, "name", credit.Name);
|
||||
AddParameter(insertCmd, "contact", credit.Contact);
|
||||
AddParameter(insertCmd, "credit_type", credit.CreditType);
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryCreditEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, name, contact, credit_type, created_at
|
||||
FROM vuln.advisory_credits
|
||||
WHERE advisory_id = @advisory_id
|
||||
ORDER BY name
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "advisory_id", advisoryId),
|
||||
MapCredit,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static AdvisoryCreditEntity MapCredit(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
AdvisoryId = reader.GetGuid(1),
|
||||
Name = reader.GetString(2),
|
||||
Contact = GetNullableString(reader, 3),
|
||||
CreditType = GetNullableString(reader, 4),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(5)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for advisory CVSS scores.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryCvssRepository : RepositoryBase<ConcelierDataSource>, IAdvisoryCvssRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public AdvisoryCvssRepository(ConcelierDataSource dataSource, ILogger<AdvisoryCvssRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryCvssEntity> scores, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_cvss WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = CreateCommand(deleteSql, connection))
|
||||
{
|
||||
deleteCmd.Transaction = transaction;
|
||||
AddParameter(deleteCmd, "advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_cvss
|
||||
(id, advisory_id, cvss_version, vector_string, base_score, base_severity,
|
||||
exploitability_score, impact_score, source, is_primary)
|
||||
VALUES
|
||||
(@id, @advisory_id, @cvss_version, @vector_string, @base_score, @base_severity,
|
||||
@exploitability_score, @impact_score, @source, @is_primary)
|
||||
""";
|
||||
|
||||
foreach (var score in scores)
|
||||
{
|
||||
await using var insertCmd = CreateCommand(insertSql, connection);
|
||||
insertCmd.Transaction = transaction;
|
||||
AddParameter(insertCmd, "id", score.Id);
|
||||
AddParameter(insertCmd, "advisory_id", advisoryId);
|
||||
AddParameter(insertCmd, "cvss_version", score.CvssVersion);
|
||||
AddParameter(insertCmd, "vector_string", score.VectorString);
|
||||
AddParameter(insertCmd, "base_score", score.BaseScore);
|
||||
AddParameter(insertCmd, "base_severity", score.BaseSeverity);
|
||||
AddParameter(insertCmd, "exploitability_score", score.ExploitabilityScore);
|
||||
AddParameter(insertCmd, "impact_score", score.ImpactScore);
|
||||
AddParameter(insertCmd, "source", score.Source);
|
||||
AddParameter(insertCmd, "is_primary", score.IsPrimary);
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryCvssEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, cvss_version, vector_string, base_score, base_severity,
|
||||
exploitability_score, impact_score, source, is_primary, created_at
|
||||
FROM vuln.advisory_cvss
|
||||
WHERE advisory_id = @advisory_id
|
||||
ORDER BY is_primary DESC, cvss_version
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "advisory_id", advisoryId),
|
||||
MapCvss,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static AdvisoryCvssEntity MapCvss(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
AdvisoryId = reader.GetGuid(1),
|
||||
CvssVersion = reader.GetString(2),
|
||||
VectorString = reader.GetString(3),
|
||||
BaseScore = reader.GetDecimal(4),
|
||||
BaseSeverity = GetNullableString(reader, 5),
|
||||
ExploitabilityScore = GetNullableDecimal(reader, 6),
|
||||
ImpactScore = GetNullableDecimal(reader, 7),
|
||||
Source = GetNullableString(reader, 8),
|
||||
IsPrimary = reader.GetBoolean(9),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(10)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for advisory references.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryReferenceRepository : RepositoryBase<ConcelierDataSource>, IAdvisoryReferenceRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public AdvisoryReferenceRepository(ConcelierDataSource dataSource, ILogger<AdvisoryReferenceRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryReferenceEntity> references, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_references WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = CreateCommand(deleteSql, connection))
|
||||
{
|
||||
deleteCmd.Transaction = transaction;
|
||||
AddParameter(deleteCmd, "advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_references
|
||||
(id, advisory_id, ref_type, url)
|
||||
VALUES
|
||||
(@id, @advisory_id, @ref_type, @url)
|
||||
""";
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
await using var insertCmd = CreateCommand(insertSql, connection);
|
||||
insertCmd.Transaction = transaction;
|
||||
AddParameter(insertCmd, "id", reference.Id);
|
||||
AddParameter(insertCmd, "advisory_id", advisoryId);
|
||||
AddParameter(insertCmd, "ref_type", reference.RefType);
|
||||
AddParameter(insertCmd, "url", reference.Url);
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryReferenceEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, ref_type, url, created_at
|
||||
FROM vuln.advisory_references
|
||||
WHERE advisory_id = @advisory_id
|
||||
ORDER BY ref_type, url
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "advisory_id", advisoryId),
|
||||
MapReference,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static AdvisoryReferenceEntity MapReference(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
AdvisoryId = reader.GetGuid(1),
|
||||
RefType = reader.GetString(2),
|
||||
Url = reader.GetString(3),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(4)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,55 +25,24 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AdvisoryEntity> UpsertAsync(AdvisoryEntity advisory, CancellationToken cancellationToken = default)
|
||||
public Task<AdvisoryEntity> UpsertAsync(
|
||||
AdvisoryEntity advisory,
|
||||
IEnumerable<AdvisoryAliasEntity>? aliases,
|
||||
IEnumerable<AdvisoryCvssEntity>? cvss,
|
||||
IEnumerable<AdvisoryAffectedEntity>? affected,
|
||||
IEnumerable<AdvisoryReferenceEntity>? references,
|
||||
IEnumerable<AdvisoryCreditEntity>? credits,
|
||||
IEnumerable<AdvisoryWeaknessEntity>? weaknesses,
|
||||
IEnumerable<KevFlagEntity>? kevFlags,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO vuln.advisories (
|
||||
id, advisory_key, primary_vuln_id, source_id, title, summary, description,
|
||||
severity, published_at, modified_at, withdrawn_at, provenance, raw_payload
|
||||
)
|
||||
VALUES (
|
||||
@id, @advisory_key, @primary_vuln_id, @source_id, @title, @summary, @description,
|
||||
@severity, @published_at, @modified_at, @withdrawn_at, @provenance::jsonb, @raw_payload::jsonb
|
||||
)
|
||||
ON CONFLICT (advisory_key) DO UPDATE SET
|
||||
primary_vuln_id = EXCLUDED.primary_vuln_id,
|
||||
source_id = COALESCE(EXCLUDED.source_id, vuln.advisories.source_id),
|
||||
title = COALESCE(EXCLUDED.title, vuln.advisories.title),
|
||||
summary = COALESCE(EXCLUDED.summary, vuln.advisories.summary),
|
||||
description = COALESCE(EXCLUDED.description, vuln.advisories.description),
|
||||
severity = COALESCE(EXCLUDED.severity, vuln.advisories.severity),
|
||||
published_at = COALESCE(EXCLUDED.published_at, vuln.advisories.published_at),
|
||||
modified_at = COALESCE(EXCLUDED.modified_at, vuln.advisories.modified_at),
|
||||
withdrawn_at = EXCLUDED.withdrawn_at,
|
||||
provenance = vuln.advisories.provenance || EXCLUDED.provenance,
|
||||
raw_payload = EXCLUDED.raw_payload
|
||||
RETURNING id, advisory_key, primary_vuln_id, source_id, title, summary, description,
|
||||
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
|
||||
created_at, updated_at
|
||||
""";
|
||||
return UpsertInternalAsync(advisory, aliases, cvss, affected, references, credits, weaknesses, kevFlags, cancellationToken);
|
||||
}
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
|
||||
AddParameter(command, "id", advisory.Id);
|
||||
AddParameter(command, "advisory_key", advisory.AdvisoryKey);
|
||||
AddParameter(command, "primary_vuln_id", advisory.PrimaryVulnId);
|
||||
AddParameter(command, "source_id", advisory.SourceId);
|
||||
AddParameter(command, "title", advisory.Title);
|
||||
AddParameter(command, "summary", advisory.Summary);
|
||||
AddParameter(command, "description", advisory.Description);
|
||||
AddParameter(command, "severity", advisory.Severity);
|
||||
AddParameter(command, "published_at", advisory.PublishedAt);
|
||||
AddParameter(command, "modified_at", advisory.ModifiedAt);
|
||||
AddParameter(command, "withdrawn_at", advisory.WithdrawnAt);
|
||||
AddJsonbParameter(command, "provenance", advisory.Provenance);
|
||||
AddJsonbParameter(command, "raw_payload", advisory.RawPayload);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return MapAdvisory(reader);
|
||||
/// <inheritdoc />
|
||||
public Task<AdvisoryEntity> UpsertAsync(AdvisoryEntity advisory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return UpsertInternalAsync(advisory, null, null, null, null, null, null, null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -133,6 +102,90 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<AdvisoryEntity>> GetByAliasAsync(string aliasValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT a.id, a.advisory_key, a.primary_vuln_id, a.source_id, a.title, a.summary, a.description,
|
||||
a.severity, a.published_at, a.modified_at, a.withdrawn_at, a.provenance::text, a.raw_payload::text,
|
||||
a.created_at, a.updated_at
|
||||
FROM vuln.advisories a
|
||||
JOIN vuln.advisory_aliases al ON al.advisory_id = a.id
|
||||
WHERE al.alias_value = @alias_value
|
||||
ORDER BY a.modified_at DESC, a.id
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "alias_value", aliasValue),
|
||||
MapAdvisory,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<AdvisoryEntity>> GetAffectingPackageAsync(
|
||||
string purl,
|
||||
int limit = 100,
|
||||
int offset = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT a.id, a.advisory_key, a.primary_vuln_id, a.source_id, a.title, a.summary, a.description,
|
||||
a.severity, a.published_at, a.modified_at, a.withdrawn_at, a.provenance::text, a.raw_payload::text,
|
||||
a.created_at, a.updated_at
|
||||
FROM vuln.advisories a
|
||||
JOIN vuln.advisory_affected af ON af.advisory_id = a.id
|
||||
WHERE af.purl = @purl
|
||||
ORDER BY a.modified_at DESC, a.id
|
||||
LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "purl", purl);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
},
|
||||
MapAdvisory,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<AdvisoryEntity>> GetAffectingPackageNameAsync(
|
||||
string ecosystem,
|
||||
string packageName,
|
||||
int limit = 100,
|
||||
int offset = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT a.id, a.advisory_key, a.primary_vuln_id, a.source_id, a.title, a.summary, a.description,
|
||||
a.severity, a.published_at, a.modified_at, a.withdrawn_at, a.provenance::text, a.raw_payload::text,
|
||||
a.created_at, a.updated_at
|
||||
FROM vuln.advisories a
|
||||
JOIN vuln.advisory_affected af ON af.advisory_id = a.id
|
||||
WHERE af.ecosystem = @ecosystem AND af.package_name = @package_name
|
||||
ORDER BY a.modified_at DESC, a.id
|
||||
LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "ecosystem", ecosystem);
|
||||
AddParameter(cmd, "package_name", packageName);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
},
|
||||
MapAdvisory,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<AdvisoryEntity>> SearchAsync(
|
||||
string query,
|
||||
@@ -299,6 +352,358 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
|
||||
return results.ToDictionary(r => r.Severity, r => r.Count);
|
||||
}
|
||||
|
||||
private async Task<AdvisoryEntity> UpsertInternalAsync(
|
||||
AdvisoryEntity advisory,
|
||||
IEnumerable<AdvisoryAliasEntity>? aliases,
|
||||
IEnumerable<AdvisoryCvssEntity>? cvss,
|
||||
IEnumerable<AdvisoryAffectedEntity>? affected,
|
||||
IEnumerable<AdvisoryReferenceEntity>? references,
|
||||
IEnumerable<AdvisoryCreditEntity>? credits,
|
||||
IEnumerable<AdvisoryWeaknessEntity>? weaknesses,
|
||||
IEnumerable<KevFlagEntity>? kevFlags,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO vuln.advisories (
|
||||
id, advisory_key, primary_vuln_id, source_id, title, summary, description,
|
||||
severity, published_at, modified_at, withdrawn_at, provenance, raw_payload
|
||||
)
|
||||
VALUES (
|
||||
@id, @advisory_key, @primary_vuln_id, @source_id, @title, @summary, @description,
|
||||
@severity, @published_at, @modified_at, @withdrawn_at, @provenance::jsonb, @raw_payload::jsonb
|
||||
)
|
||||
ON CONFLICT (advisory_key) DO UPDATE SET
|
||||
primary_vuln_id = EXCLUDED.primary_vuln_id,
|
||||
source_id = COALESCE(EXCLUDED.source_id, vuln.advisories.source_id),
|
||||
title = COALESCE(EXCLUDED.title, vuln.advisories.title),
|
||||
summary = COALESCE(EXCLUDED.summary, vuln.advisories.summary),
|
||||
description = COALESCE(EXCLUDED.description, vuln.advisories.description),
|
||||
severity = COALESCE(EXCLUDED.severity, vuln.advisories.severity),
|
||||
published_at = COALESCE(EXCLUDED.published_at, vuln.advisories.published_at),
|
||||
modified_at = COALESCE(EXCLUDED.modified_at, vuln.advisories.modified_at),
|
||||
withdrawn_at = EXCLUDED.withdrawn_at,
|
||||
provenance = vuln.advisories.provenance || EXCLUDED.provenance,
|
||||
raw_payload = EXCLUDED.raw_payload,
|
||||
updated_at = NOW()
|
||||
RETURNING id, advisory_key, primary_vuln_id, source_id, title, summary, description,
|
||||
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
|
||||
created_at, updated_at
|
||||
""";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
AdvisoryEntity result;
|
||||
await using (var command = CreateCommand(sql, connection))
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
AddParameter(command, "id", advisory.Id);
|
||||
AddParameter(command, "advisory_key", advisory.AdvisoryKey);
|
||||
AddParameter(command, "primary_vuln_id", advisory.PrimaryVulnId);
|
||||
AddParameter(command, "source_id", advisory.SourceId);
|
||||
AddParameter(command, "title", advisory.Title);
|
||||
AddParameter(command, "summary", advisory.Summary);
|
||||
AddParameter(command, "description", advisory.Description);
|
||||
AddParameter(command, "severity", advisory.Severity);
|
||||
AddParameter(command, "published_at", advisory.PublishedAt);
|
||||
AddParameter(command, "modified_at", advisory.ModifiedAt);
|
||||
AddParameter(command, "withdrawn_at", advisory.WithdrawnAt);
|
||||
AddJsonbParameter(command, "provenance", advisory.Provenance);
|
||||
AddJsonbParameter(command, "raw_payload", advisory.RawPayload);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
result = MapAdvisory(reader);
|
||||
}
|
||||
|
||||
// Replace child tables only when collections are provided (null = leave existing).
|
||||
if (aliases is not null)
|
||||
{
|
||||
await ReplaceAliasesAsync(result.Id, aliases, connection, transaction, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
if (cvss is not null)
|
||||
{
|
||||
await ReplaceCvssAsync(result.Id, cvss, connection, transaction, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
if (affected is not null)
|
||||
{
|
||||
await ReplaceAffectedAsync(result.Id, affected, connection, transaction, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
if (references is not null)
|
||||
{
|
||||
await ReplaceReferencesAsync(result.Id, references, connection, transaction, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
if (credits is not null)
|
||||
{
|
||||
await ReplaceCreditsAsync(result.Id, credits, connection, transaction, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
if (weaknesses is not null)
|
||||
{
|
||||
await ReplaceWeaknessesAsync(result.Id, weaknesses, connection, transaction, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
if (kevFlags is not null)
|
||||
{
|
||||
await ReplaceKevFlagsAsync(result.Id, kevFlags, connection, transaction, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task ReplaceAliasesAsync(
|
||||
Guid advisoryId,
|
||||
IEnumerable<AdvisoryAliasEntity> aliases,
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction transaction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_aliases WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = new NpgsqlCommand(deleteSql, connection, transaction))
|
||||
{
|
||||
deleteCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_aliases (id, advisory_id, alias_type, alias_value, is_primary)
|
||||
VALUES (@id, @advisory_id, @alias_type, @alias_value, @is_primary)
|
||||
""";
|
||||
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
await using var insertCmd = new NpgsqlCommand(insertSql, connection, transaction);
|
||||
insertCmd.Parameters.AddWithValue("id", alias.Id);
|
||||
insertCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
insertCmd.Parameters.AddWithValue("alias_type", alias.AliasType);
|
||||
insertCmd.Parameters.AddWithValue("alias_value", alias.AliasValue);
|
||||
insertCmd.Parameters.AddWithValue("is_primary", alias.IsPrimary);
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReplaceCvssAsync(
|
||||
Guid advisoryId,
|
||||
IEnumerable<AdvisoryCvssEntity> scores,
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction transaction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_cvss WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = new NpgsqlCommand(deleteSql, connection, transaction))
|
||||
{
|
||||
deleteCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_cvss
|
||||
(id, advisory_id, cvss_version, vector_string, base_score, base_severity,
|
||||
exploitability_score, impact_score, source, is_primary)
|
||||
VALUES
|
||||
(@id, @advisory_id, @cvss_version, @vector_string, @base_score, @base_severity,
|
||||
@exploitability_score, @impact_score, @source, @is_primary)
|
||||
""";
|
||||
|
||||
foreach (var score in scores)
|
||||
{
|
||||
await using var insertCmd = new NpgsqlCommand(insertSql, connection, transaction);
|
||||
insertCmd.Parameters.AddWithValue("id", score.Id);
|
||||
insertCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
insertCmd.Parameters.AddWithValue("cvss_version", score.CvssVersion);
|
||||
insertCmd.Parameters.AddWithValue("vector_string", score.VectorString);
|
||||
insertCmd.Parameters.AddWithValue("base_score", score.BaseScore);
|
||||
insertCmd.Parameters.AddWithValue("base_severity", (object?)score.BaseSeverity ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("exploitability_score", (object?)score.ExploitabilityScore ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("impact_score", (object?)score.ImpactScore ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("source", (object?)score.Source ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("is_primary", score.IsPrimary);
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReplaceAffectedAsync(
|
||||
Guid advisoryId,
|
||||
IEnumerable<AdvisoryAffectedEntity> affected,
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction transaction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_affected WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = new NpgsqlCommand(deleteSql, connection, transaction))
|
||||
{
|
||||
deleteCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_affected
|
||||
(id, advisory_id, ecosystem, package_name, purl, version_range, versions_affected,
|
||||
versions_fixed, database_specific)
|
||||
VALUES
|
||||
(@id, @advisory_id, @ecosystem, @package_name, @purl, @version_range::jsonb,
|
||||
@versions_affected, @versions_fixed, @database_specific::jsonb)
|
||||
""";
|
||||
|
||||
foreach (var entry in affected)
|
||||
{
|
||||
await using var insertCmd = new NpgsqlCommand(insertSql, connection, transaction);
|
||||
insertCmd.Parameters.AddWithValue("id", entry.Id);
|
||||
insertCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
insertCmd.Parameters.AddWithValue("ecosystem", entry.Ecosystem);
|
||||
insertCmd.Parameters.AddWithValue("package_name", entry.PackageName);
|
||||
insertCmd.Parameters.AddWithValue("purl", (object?)entry.Purl ?? DBNull.Value);
|
||||
insertCmd.Parameters.Add(new NpgsqlParameter<string?>("version_range", NpgsqlTypes.NpgsqlDbType.Jsonb)
|
||||
{
|
||||
TypedValue = entry.VersionRange
|
||||
});
|
||||
insertCmd.Parameters.Add(new NpgsqlParameter<string[]?>("versions_affected", NpgsqlTypes.NpgsqlDbType.Array | NpgsqlTypes.NpgsqlDbType.Text)
|
||||
{
|
||||
TypedValue = entry.VersionsAffected
|
||||
});
|
||||
insertCmd.Parameters.Add(new NpgsqlParameter<string[]?>("versions_fixed", NpgsqlTypes.NpgsqlDbType.Array | NpgsqlTypes.NpgsqlDbType.Text)
|
||||
{
|
||||
TypedValue = entry.VersionsFixed
|
||||
});
|
||||
insertCmd.Parameters.Add(new NpgsqlParameter<string?>("database_specific", NpgsqlTypes.NpgsqlDbType.Jsonb)
|
||||
{
|
||||
TypedValue = entry.DatabaseSpecific
|
||||
});
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReplaceReferencesAsync(
|
||||
Guid advisoryId,
|
||||
IEnumerable<AdvisoryReferenceEntity> references,
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction transaction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_references WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = new NpgsqlCommand(deleteSql, connection, transaction))
|
||||
{
|
||||
deleteCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_references (id, advisory_id, ref_type, url)
|
||||
VALUES (@id, @advisory_id, @ref_type, @url)
|
||||
""";
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
await using var insertCmd = new NpgsqlCommand(insertSql, connection, transaction);
|
||||
insertCmd.Parameters.AddWithValue("id", reference.Id);
|
||||
insertCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
insertCmd.Parameters.AddWithValue("ref_type", reference.RefType);
|
||||
insertCmd.Parameters.AddWithValue("url", reference.Url);
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReplaceCreditsAsync(
|
||||
Guid advisoryId,
|
||||
IEnumerable<AdvisoryCreditEntity> credits,
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction transaction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_credits WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = new NpgsqlCommand(deleteSql, connection, transaction))
|
||||
{
|
||||
deleteCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_credits (id, advisory_id, name, contact, credit_type)
|
||||
VALUES (@id, @advisory_id, @name, @contact, @credit_type)
|
||||
""";
|
||||
|
||||
foreach (var credit in credits)
|
||||
{
|
||||
await using var insertCmd = new NpgsqlCommand(insertSql, connection, transaction);
|
||||
insertCmd.Parameters.AddWithValue("id", credit.Id);
|
||||
insertCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
insertCmd.Parameters.AddWithValue("name", credit.Name);
|
||||
insertCmd.Parameters.AddWithValue("contact", (object?)credit.Contact ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("credit_type", (object?)credit.CreditType ?? DBNull.Value);
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReplaceWeaknessesAsync(
|
||||
Guid advisoryId,
|
||||
IEnumerable<AdvisoryWeaknessEntity> weaknesses,
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction transaction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_weaknesses WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = new NpgsqlCommand(deleteSql, connection, transaction))
|
||||
{
|
||||
deleteCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_weaknesses (id, advisory_id, cwe_id, description, source)
|
||||
VALUES (@id, @advisory_id, @cwe_id, @description, @source)
|
||||
""";
|
||||
|
||||
foreach (var weakness in weaknesses)
|
||||
{
|
||||
await using var insertCmd = new NpgsqlCommand(insertSql, connection, transaction);
|
||||
insertCmd.Parameters.AddWithValue("id", weakness.Id);
|
||||
insertCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
insertCmd.Parameters.AddWithValue("cwe_id", weakness.CweId);
|
||||
insertCmd.Parameters.AddWithValue("description", (object?)weakness.Description ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("source", (object?)weakness.Source ?? DBNull.Value);
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReplaceKevFlagsAsync(
|
||||
Guid advisoryId,
|
||||
IEnumerable<KevFlagEntity> kevFlags,
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction transaction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string deleteSql = "DELETE FROM vuln.kev_flags WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = new NpgsqlCommand(deleteSql, connection, transaction))
|
||||
{
|
||||
deleteCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.kev_flags
|
||||
(id, advisory_id, cve_id, vendor_project, product, vulnerability_name,
|
||||
date_added, due_date, known_ransomware_use, notes)
|
||||
VALUES
|
||||
(@id, @advisory_id, @cve_id, @vendor_project, @product, @vulnerability_name,
|
||||
@date_added, @due_date, @known_ransomware_use, @notes)
|
||||
""";
|
||||
|
||||
foreach (var flag in kevFlags)
|
||||
{
|
||||
await using var insertCmd = new NpgsqlCommand(insertSql, connection, transaction);
|
||||
insertCmd.Parameters.AddWithValue("id", flag.Id);
|
||||
insertCmd.Parameters.AddWithValue("advisory_id", advisoryId);
|
||||
insertCmd.Parameters.AddWithValue("cve_id", flag.CveId);
|
||||
insertCmd.Parameters.AddWithValue("vendor_project", (object?)flag.VendorProject ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("product", (object?)flag.Product ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("vulnerability_name", (object?)flag.VulnerabilityName ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("date_added", flag.DateAdded);
|
||||
insertCmd.Parameters.AddWithValue("due_date", (object?)flag.DueDate ?? DBNull.Value);
|
||||
insertCmd.Parameters.AddWithValue("known_ransomware_use", flag.KnownRansomwareUse);
|
||||
insertCmd.Parameters.AddWithValue("notes", (object?)flag.Notes ?? DBNull.Value);
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static AdvisoryEntity MapAdvisory(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for advisory snapshots.
|
||||
/// </summary>
|
||||
public sealed class AdvisorySnapshotRepository : RepositoryBase<ConcelierDataSource>, IAdvisorySnapshotRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public AdvisorySnapshotRepository(ConcelierDataSource dataSource, ILogger<AdvisorySnapshotRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<AdvisorySnapshotEntity> InsertAsync(AdvisorySnapshotEntity snapshot, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO vuln.advisory_snapshots
|
||||
(id, feed_snapshot_id, advisory_key, content_hash)
|
||||
VALUES
|
||||
(@id, @feed_snapshot_id, @advisory_key, @content_hash)
|
||||
ON CONFLICT (feed_snapshot_id, advisory_key) DO UPDATE SET
|
||||
content_hash = EXCLUDED.content_hash
|
||||
RETURNING id, feed_snapshot_id, advisory_key, content_hash, created_at
|
||||
""";
|
||||
|
||||
return await QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "id", snapshot.Id);
|
||||
AddParameter(cmd, "feed_snapshot_id", snapshot.FeedSnapshotId);
|
||||
AddParameter(cmd, "advisory_key", snapshot.AdvisoryKey);
|
||||
AddParameter(cmd, "content_hash", snapshot.ContentHash);
|
||||
},
|
||||
MapSnapshot!,
|
||||
cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("Insert returned null");
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisorySnapshotEntity>> GetByFeedSnapshotAsync(Guid feedSnapshotId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, feed_snapshot_id, advisory_key, content_hash, created_at
|
||||
FROM vuln.advisory_snapshots
|
||||
WHERE feed_snapshot_id = @feed_snapshot_id
|
||||
ORDER BY advisory_key
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "feed_snapshot_id", feedSnapshotId),
|
||||
MapSnapshot,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static AdvisorySnapshotEntity MapSnapshot(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
FeedSnapshotId = reader.GetGuid(1),
|
||||
AdvisoryKey = reader.GetString(2),
|
||||
ContentHash = reader.GetString(3),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(4)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for advisory weaknesses (CWE).
|
||||
/// </summary>
|
||||
public sealed class AdvisoryWeaknessRepository : RepositoryBase<ConcelierDataSource>, IAdvisoryWeaknessRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public AdvisoryWeaknessRepository(ConcelierDataSource dataSource, ILogger<AdvisoryWeaknessRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryWeaknessEntity> weaknesses, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string deleteSql = "DELETE FROM vuln.advisory_weaknesses WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = CreateCommand(deleteSql, connection))
|
||||
{
|
||||
deleteCmd.Transaction = transaction;
|
||||
AddParameter(deleteCmd, "advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.advisory_weaknesses
|
||||
(id, advisory_id, cwe_id, description, source)
|
||||
VALUES
|
||||
(@id, @advisory_id, @cwe_id, @description, @source)
|
||||
""";
|
||||
|
||||
foreach (var weakness in weaknesses)
|
||||
{
|
||||
await using var insertCmd = CreateCommand(insertSql, connection);
|
||||
insertCmd.Transaction = transaction;
|
||||
AddParameter(insertCmd, "id", weakness.Id);
|
||||
AddParameter(insertCmd, "advisory_id", advisoryId);
|
||||
AddParameter(insertCmd, "cwe_id", weakness.CweId);
|
||||
AddParameter(insertCmd, "description", weakness.Description);
|
||||
AddParameter(insertCmd, "source", weakness.Source);
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryWeaknessEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, cwe_id, description, source, created_at
|
||||
FROM vuln.advisory_weaknesses
|
||||
WHERE advisory_id = @advisory_id
|
||||
ORDER BY cwe_id
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "advisory_id", advisoryId),
|
||||
MapWeakness,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static AdvisoryWeaknessEntity MapWeakness(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
AdvisoryId = reader.GetGuid(1),
|
||||
CweId = reader.GetString(2),
|
||||
Description = GetNullableString(reader, 3),
|
||||
Source = GetNullableString(reader, 4),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(5)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for feed snapshots.
|
||||
/// </summary>
|
||||
public sealed class FeedSnapshotRepository : RepositoryBase<ConcelierDataSource>, IFeedSnapshotRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public FeedSnapshotRepository(ConcelierDataSource dataSource, ILogger<FeedSnapshotRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<FeedSnapshotEntity> InsertAsync(FeedSnapshotEntity snapshot, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO vuln.feed_snapshots
|
||||
(id, source_id, snapshot_id, advisory_count, checksum, metadata)
|
||||
VALUES
|
||||
(@id, @source_id, @snapshot_id, @advisory_count, @checksum, @metadata::jsonb)
|
||||
ON CONFLICT (source_id, snapshot_id) DO NOTHING
|
||||
RETURNING id, source_id, snapshot_id, advisory_count, checksum, metadata::text, created_at
|
||||
""";
|
||||
|
||||
return await QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "id", snapshot.Id);
|
||||
AddParameter(cmd, "source_id", snapshot.SourceId);
|
||||
AddParameter(cmd, "snapshot_id", snapshot.SnapshotId);
|
||||
AddParameter(cmd, "advisory_count", snapshot.AdvisoryCount);
|
||||
AddParameter(cmd, "checksum", snapshot.Checksum);
|
||||
AddJsonbParameter(cmd, "metadata", snapshot.Metadata);
|
||||
},
|
||||
MapSnapshot!,
|
||||
cancellationToken).ConfigureAwait(false) ?? snapshot;
|
||||
}
|
||||
|
||||
public Task<FeedSnapshotEntity?> GetBySourceAndIdAsync(Guid sourceId, string snapshotId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, source_id, snapshot_id, advisory_count, checksum, metadata::text, created_at
|
||||
FROM vuln.feed_snapshots
|
||||
WHERE source_id = @source_id AND snapshot_id = @snapshot_id
|
||||
""";
|
||||
|
||||
return QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "source_id", sourceId);
|
||||
AddParameter(cmd, "snapshot_id", snapshotId);
|
||||
},
|
||||
MapSnapshot,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static FeedSnapshotEntity MapSnapshot(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
SourceId = reader.GetGuid(1),
|
||||
SnapshotId = reader.GetString(2),
|
||||
AdvisoryCount = reader.GetInt32(3),
|
||||
Checksum = GetNullableString(reader, 4),
|
||||
Metadata = reader.GetString(5),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(6)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for advisory affected package rows.
|
||||
/// </summary>
|
||||
public interface IAdvisoryAffectedRepository
|
||||
{
|
||||
Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryAffectedEntity> affected, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryAffectedEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryAffectedEntity>> GetByPurlAsync(string purl, int limit = 100, int offset = 0, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryAffectedEntity>> GetByPackageNameAsync(string ecosystem, string packageName, int limit = 100, int offset = 0, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for advisory aliases.
|
||||
/// </summary>
|
||||
public interface IAdvisoryAliasRepository
|
||||
{
|
||||
Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryAliasEntity> aliases, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryAliasEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryAliasEntity>> GetByAliasAsync(string aliasValue, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for advisory credits.
|
||||
/// </summary>
|
||||
public interface IAdvisoryCreditRepository
|
||||
{
|
||||
Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryCreditEntity> credits, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryCreditEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for advisory CVSS scores.
|
||||
/// </summary>
|
||||
public interface IAdvisoryCvssRepository
|
||||
{
|
||||
Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryCvssEntity> scores, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryCvssEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for advisory references.
|
||||
/// </summary>
|
||||
public interface IAdvisoryReferenceRepository
|
||||
{
|
||||
Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryReferenceEntity> references, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryReferenceEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,30 @@ public interface IAdvisoryRepository
|
||||
/// </summary>
|
||||
Task<AdvisoryEntity?> GetByVulnIdAsync(string vulnId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets advisories that include the provided alias (e.g., CVE, GHSA).
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<AdvisoryEntity>> GetByAliasAsync(string aliasValue, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets advisories affecting a package identified by full PURL.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<AdvisoryEntity>> GetAffectingPackageAsync(
|
||||
string purl,
|
||||
int limit = 100,
|
||||
int offset = 0,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets advisories affecting a package by ecosystem/name (when PURL missing).
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<AdvisoryEntity>> GetAffectingPackageNameAsync(
|
||||
string ecosystem,
|
||||
string packageName,
|
||||
int limit = 100,
|
||||
int offset = 0,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Searches advisories by full-text search.
|
||||
/// </summary>
|
||||
@@ -72,4 +96,19 @@ public interface IAdvisoryRepository
|
||||
/// Counts advisories by severity.
|
||||
/// </summary>
|
||||
Task<IDictionary<string, long>> CountBySeverityAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Upserts an advisory and optional child records in a single transaction.
|
||||
/// Passing null collections leaves existing child rows untouched; pass empty collections to replace with none.
|
||||
/// </summary>
|
||||
Task<AdvisoryEntity> UpsertAsync(
|
||||
AdvisoryEntity advisory,
|
||||
IEnumerable<Models.AdvisoryAliasEntity>? aliases,
|
||||
IEnumerable<Models.AdvisoryCvssEntity>? cvss,
|
||||
IEnumerable<Models.AdvisoryAffectedEntity>? affected,
|
||||
IEnumerable<Models.AdvisoryReferenceEntity>? references,
|
||||
IEnumerable<Models.AdvisoryCreditEntity>? credits,
|
||||
IEnumerable<Models.AdvisoryWeaknessEntity>? weaknesses,
|
||||
IEnumerable<Models.KevFlagEntity>? kevFlags,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for advisory snapshots.
|
||||
/// </summary>
|
||||
public interface IAdvisorySnapshotRepository
|
||||
{
|
||||
Task<AdvisorySnapshotEntity> InsertAsync(AdvisorySnapshotEntity snapshot, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisorySnapshotEntity>> GetByFeedSnapshotAsync(Guid feedSnapshotId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for advisory weaknesses (CWE).
|
||||
/// </summary>
|
||||
public interface IAdvisoryWeaknessRepository
|
||||
{
|
||||
Task ReplaceAsync(Guid advisoryId, IEnumerable<AdvisoryWeaknessEntity> weaknesses, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvisoryWeaknessEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for feed snapshots.
|
||||
/// </summary>
|
||||
public interface IFeedSnapshotRepository
|
||||
{
|
||||
Task<FeedSnapshotEntity> InsertAsync(FeedSnapshotEntity snapshot, CancellationToken cancellationToken = default);
|
||||
Task<FeedSnapshotEntity?> GetBySourceAndIdAsync(Guid sourceId, string snapshotId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for KEV flag records.
|
||||
/// </summary>
|
||||
public interface IKevFlagRepository
|
||||
{
|
||||
Task ReplaceAsync(Guid advisoryId, IEnumerable<KevFlagEntity> flags, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<KevFlagEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<KevFlagEntity>> GetByCveAsync(string cveId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for merge event audit records.
|
||||
/// </summary>
|
||||
public interface IMergeEventRepository
|
||||
{
|
||||
Task<MergeEventEntity> InsertAsync(MergeEventEntity evt, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<MergeEventEntity>> GetByAdvisoryAsync(Guid advisoryId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for vulnerability feed sources.
|
||||
/// </summary>
|
||||
public interface ISourceRepository
|
||||
{
|
||||
Task<SourceEntity> UpsertAsync(SourceEntity source, CancellationToken cancellationToken = default);
|
||||
Task<SourceEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<SourceEntity?> GetByKeyAsync(string key, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<SourceEntity>> ListAsync(bool? enabled = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for source ingestion state.
|
||||
/// </summary>
|
||||
public interface ISourceStateRepository
|
||||
{
|
||||
Task<SourceStateEntity> UpsertAsync(SourceStateEntity state, CancellationToken cancellationToken = default);
|
||||
Task<SourceStateEntity?> GetBySourceIdAsync(Guid sourceId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for KEV flags.
|
||||
/// </summary>
|
||||
public sealed class KevFlagRepository : RepositoryBase<ConcelierDataSource>, IKevFlagRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public KevFlagRepository(ConcelierDataSource dataSource, ILogger<KevFlagRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Guid advisoryId, IEnumerable<KevFlagEntity> flags, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string deleteSql = "DELETE FROM vuln.kev_flags WHERE advisory_id = @advisory_id";
|
||||
await using (var deleteCmd = CreateCommand(deleteSql, connection))
|
||||
{
|
||||
deleteCmd.Transaction = transaction;
|
||||
AddParameter(deleteCmd, "advisory_id", advisoryId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO vuln.kev_flags
|
||||
(id, advisory_id, cve_id, vendor_project, product, vulnerability_name,
|
||||
date_added, due_date, known_ransomware_use, notes)
|
||||
VALUES
|
||||
(@id, @advisory_id, @cve_id, @vendor_project, @product, @vulnerability_name,
|
||||
@date_added, @due_date, @known_ransomware_use, @notes)
|
||||
""";
|
||||
|
||||
foreach (var flag in flags)
|
||||
{
|
||||
await using var insertCmd = CreateCommand(insertSql, connection);
|
||||
insertCmd.Transaction = transaction;
|
||||
AddParameter(insertCmd, "id", flag.Id);
|
||||
AddParameter(insertCmd, "advisory_id", advisoryId);
|
||||
AddParameter(insertCmd, "cve_id", flag.CveId);
|
||||
AddParameter(insertCmd, "vendor_project", flag.VendorProject);
|
||||
AddParameter(insertCmd, "product", flag.Product);
|
||||
AddParameter(insertCmd, "vulnerability_name", flag.VulnerabilityName);
|
||||
AddParameter(insertCmd, "date_added", flag.DateAdded);
|
||||
AddParameter(insertCmd, "due_date", flag.DueDate);
|
||||
AddParameter(insertCmd, "known_ransomware_use", flag.KnownRansomwareUse);
|
||||
AddParameter(insertCmd, "notes", flag.Notes);
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<KevFlagEntity>> GetByAdvisoryAsync(Guid advisoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, cve_id, vendor_project, product, vulnerability_name,
|
||||
date_added, due_date, known_ransomware_use, notes, created_at
|
||||
FROM vuln.kev_flags
|
||||
WHERE advisory_id = @advisory_id
|
||||
ORDER BY date_added DESC, cve_id
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "advisory_id", advisoryId),
|
||||
MapKev,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<KevFlagEntity>> GetByCveAsync(string cveId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, cve_id, vendor_project, product, vulnerability_name,
|
||||
date_added, due_date, known_ransomware_use, notes, created_at
|
||||
FROM vuln.kev_flags
|
||||
WHERE cve_id = @cve_id
|
||||
ORDER BY date_added DESC, advisory_id
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "cve_id", cveId),
|
||||
MapKev,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static KevFlagEntity MapKev(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
AdvisoryId = reader.GetGuid(1),
|
||||
CveId = reader.GetString(2),
|
||||
VendorProject = GetNullableString(reader, 3),
|
||||
Product = GetNullableString(reader, 4),
|
||||
VulnerabilityName = GetNullableString(reader, 5),
|
||||
DateAdded = DateOnly.FromDateTime(reader.GetDateTime(6)),
|
||||
DueDate = reader.IsDBNull(7) ? null : DateOnly.FromDateTime(reader.GetDateTime(7)),
|
||||
KnownRansomwareUse = reader.GetBoolean(8),
|
||||
Notes = GetNullableString(reader, 9),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(10)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for merge event audit records.
|
||||
/// </summary>
|
||||
public sealed class MergeEventRepository : RepositoryBase<ConcelierDataSource>, IMergeEventRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public MergeEventRepository(ConcelierDataSource dataSource, ILogger<MergeEventRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<MergeEventEntity> InsertAsync(MergeEventEntity evt, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO vuln.merge_events
|
||||
(advisory_id, source_id, event_type, old_value, new_value)
|
||||
VALUES
|
||||
(@advisory_id, @source_id, @event_type, @old_value::jsonb, @new_value::jsonb)
|
||||
RETURNING id, advisory_id, source_id, event_type, old_value::text, new_value::text, created_at
|
||||
""";
|
||||
|
||||
return await QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "advisory_id", evt.AdvisoryId);
|
||||
AddParameter(cmd, "source_id", evt.SourceId);
|
||||
AddParameter(cmd, "event_type", evt.EventType);
|
||||
AddJsonbParameter(cmd, "old_value", evt.OldValue);
|
||||
AddJsonbParameter(cmd, "new_value", evt.NewValue);
|
||||
},
|
||||
MapEvent!,
|
||||
cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("Insert returned null");
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<MergeEventEntity>> GetByAdvisoryAsync(Guid advisoryId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, advisory_id, source_id, event_type, old_value::text, new_value::text, created_at
|
||||
FROM vuln.merge_events
|
||||
WHERE advisory_id = @advisory_id
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "advisory_id", advisoryId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
},
|
||||
MapEvent,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static MergeEventEntity MapEvent(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetInt64(0),
|
||||
AdvisoryId = reader.GetGuid(1),
|
||||
SourceId = GetNullableGuid(reader, 2),
|
||||
EventType = reader.GetString(3),
|
||||
OldValue = GetNullableString(reader, 4),
|
||||
NewValue = GetNullableString(reader, 5),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(6)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for feed sources.
|
||||
/// </summary>
|
||||
public sealed class SourceRepository : RepositoryBase<ConcelierDataSource>, ISourceRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public SourceRepository(ConcelierDataSource dataSource, ILogger<SourceRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<SourceEntity> UpsertAsync(SourceEntity source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO vuln.sources
|
||||
(id, key, name, source_type, url, priority, enabled, config, metadata)
|
||||
VALUES
|
||||
(@id, @key, @name, @source_type, @url, @priority, @enabled, @config::jsonb, @metadata::jsonb)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
source_type = EXCLUDED.source_type,
|
||||
url = EXCLUDED.url,
|
||||
priority = EXCLUDED.priority,
|
||||
enabled = EXCLUDED.enabled,
|
||||
config = EXCLUDED.config,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = NOW()
|
||||
RETURNING id, key, name, source_type, url, priority, enabled,
|
||||
config::text, metadata::text, created_at, updated_at
|
||||
""";
|
||||
|
||||
return await QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "id", source.Id);
|
||||
AddParameter(cmd, "key", source.Key);
|
||||
AddParameter(cmd, "name", source.Name);
|
||||
AddParameter(cmd, "source_type", source.SourceType);
|
||||
AddParameter(cmd, "url", source.Url);
|
||||
AddParameter(cmd, "priority", source.Priority);
|
||||
AddParameter(cmd, "enabled", source.Enabled);
|
||||
AddJsonbParameter(cmd, "config", source.Config);
|
||||
AddJsonbParameter(cmd, "metadata", source.Metadata);
|
||||
},
|
||||
MapSource!,
|
||||
cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("Upsert returned null");
|
||||
}
|
||||
|
||||
public Task<SourceEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, key, name, source_type, url, priority, enabled,
|
||||
config::text, metadata::text, created_at, updated_at
|
||||
FROM vuln.sources
|
||||
WHERE id = @id
|
||||
""";
|
||||
|
||||
return QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "id", id),
|
||||
MapSource,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<SourceEntity?> GetByKeyAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, key, name, source_type, url, priority, enabled,
|
||||
config::text, metadata::text, created_at, updated_at
|
||||
FROM vuln.sources
|
||||
WHERE key = @key
|
||||
""";
|
||||
|
||||
return QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "key", key),
|
||||
MapSource,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<SourceEntity>> ListAsync(bool? enabled = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, key, name, source_type, url, priority, enabled,
|
||||
config::text, metadata::text, created_at, updated_at
|
||||
FROM vuln.sources
|
||||
""";
|
||||
|
||||
if (enabled.HasValue)
|
||||
{
|
||||
sql += " WHERE enabled = @enabled";
|
||||
}
|
||||
|
||||
sql += " ORDER BY priority DESC, key";
|
||||
|
||||
return QueryAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
if (enabled.HasValue)
|
||||
{
|
||||
AddParameter(cmd, "enabled", enabled.Value);
|
||||
}
|
||||
},
|
||||
MapSource,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static SourceEntity MapSource(Npgsql.NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
Key = reader.GetString(1),
|
||||
Name = reader.GetString(2),
|
||||
SourceType = reader.GetString(3),
|
||||
Url = GetNullableString(reader, 4),
|
||||
Priority = reader.GetInt32(5),
|
||||
Enabled = reader.GetBoolean(6),
|
||||
Config = reader.GetString(7),
|
||||
Metadata = reader.GetString(8),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(9),
|
||||
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(10)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for source ingestion state.
|
||||
/// </summary>
|
||||
public sealed class SourceStateRepository : RepositoryBase<ConcelierDataSource>, ISourceStateRepository
|
||||
{
|
||||
private const string SystemTenantId = "_system";
|
||||
|
||||
public SourceStateRepository(ConcelierDataSource dataSource, ILogger<SourceStateRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<SourceStateEntity> UpsertAsync(SourceStateEntity state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO vuln.source_states
|
||||
(id, source_id, cursor, last_sync_at, last_success_at, last_error,
|
||||
sync_count, error_count, metadata)
|
||||
VALUES
|
||||
(@id, @source_id, @cursor, @last_sync_at, @last_success_at, @last_error,
|
||||
@sync_count, @error_count, @metadata::jsonb)
|
||||
ON CONFLICT (source_id) DO UPDATE SET
|
||||
cursor = EXCLUDED.cursor,
|
||||
last_sync_at = EXCLUDED.last_sync_at,
|
||||
last_success_at = EXCLUDED.last_success_at,
|
||||
last_error = EXCLUDED.last_error,
|
||||
sync_count = EXCLUDED.sync_count,
|
||||
error_count = EXCLUDED.error_count,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = NOW()
|
||||
RETURNING id, source_id, cursor, last_sync_at, last_success_at, last_error,
|
||||
sync_count, error_count, metadata::text, updated_at
|
||||
""";
|
||||
|
||||
return await QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "id", state.Id);
|
||||
AddParameter(cmd, "source_id", state.SourceId);
|
||||
AddParameter(cmd, "cursor", state.Cursor);
|
||||
AddParameter(cmd, "last_sync_at", state.LastSyncAt);
|
||||
AddParameter(cmd, "last_success_at", state.LastSuccessAt);
|
||||
AddParameter(cmd, "last_error", state.LastError);
|
||||
AddParameter(cmd, "sync_count", state.SyncCount);
|
||||
AddParameter(cmd, "error_count", state.ErrorCount);
|
||||
AddJsonbParameter(cmd, "metadata", state.Metadata);
|
||||
},
|
||||
MapState!,
|
||||
cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("Upsert returned null");
|
||||
}
|
||||
|
||||
public Task<SourceStateEntity?> GetBySourceIdAsync(Guid sourceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, source_id, cursor, last_sync_at, last_success_at, last_error,
|
||||
sync_count, error_count, metadata::text, updated_at
|
||||
FROM vuln.source_states
|
||||
WHERE source_id = @source_id
|
||||
""";
|
||||
|
||||
return QuerySingleOrDefaultAsync(
|
||||
SystemTenantId,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "source_id", sourceId),
|
||||
MapState,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static SourceStateEntity MapState(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
SourceId = reader.GetGuid(1),
|
||||
Cursor = GetNullableString(reader, 2),
|
||||
LastSyncAt = GetNullableDateTimeOffset(reader, 3),
|
||||
LastSuccessAt = GetNullableDateTimeOffset(reader, 4),
|
||||
LastError = GetNullableString(reader, 5),
|
||||
SyncCount = reader.GetInt64(6),
|
||||
ErrorCount = reader.GetInt32(7),
|
||||
Metadata = reader.GetString(8),
|
||||
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(9)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,6 +28,18 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
// Register repositories
|
||||
services.AddScoped<IAdvisoryRepository, AdvisoryRepository>();
|
||||
services.AddScoped<ISourceRepository, SourceRepository>();
|
||||
services.AddScoped<IAdvisoryAliasRepository, AdvisoryAliasRepository>();
|
||||
services.AddScoped<IAdvisoryCvssRepository, AdvisoryCvssRepository>();
|
||||
services.AddScoped<IAdvisoryAffectedRepository, AdvisoryAffectedRepository>();
|
||||
services.AddScoped<IAdvisoryReferenceRepository, AdvisoryReferenceRepository>();
|
||||
services.AddScoped<IAdvisoryCreditRepository, AdvisoryCreditRepository>();
|
||||
services.AddScoped<IAdvisoryWeaknessRepository, AdvisoryWeaknessRepository>();
|
||||
services.AddScoped<IKevFlagRepository, KevFlagRepository>();
|
||||
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
|
||||
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
|
||||
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
|
||||
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -47,6 +59,18 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
// Register repositories
|
||||
services.AddScoped<IAdvisoryRepository, AdvisoryRepository>();
|
||||
services.AddScoped<ISourceRepository, SourceRepository>();
|
||||
services.AddScoped<IAdvisoryAliasRepository, AdvisoryAliasRepository>();
|
||||
services.AddScoped<IAdvisoryCvssRepository, AdvisoryCvssRepository>();
|
||||
services.AddScoped<IAdvisoryAffectedRepository, AdvisoryAffectedRepository>();
|
||||
services.AddScoped<IAdvisoryReferenceRepository, AdvisoryReferenceRepository>();
|
||||
services.AddScoped<IAdvisoryCreditRepository, AdvisoryCreditRepository>();
|
||||
services.AddScoped<IAdvisoryWeaknessRepository, AdvisoryWeaknessRepository>();
|
||||
services.AddScoped<IKevFlagRepository, KevFlagRepository>();
|
||||
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
|
||||
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
|
||||
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
|
||||
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user