feat: Implement BerkeleyDB reader for RPM databases
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
console-runner-image / build-runner-image (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
wine-csp-build / Integration Tests (push) Has been cancelled
wine-csp-build / Security Scan (push) Has been cancelled
wine-csp-build / Generate SBOM (push) Has been cancelled
wine-csp-build / Publish Image (push) Has been cancelled
wine-csp-build / Air-Gap Bundle (push) Has been cancelled
wine-csp-build / Test Summary (push) Has been cancelled

- Added BerkeleyDbReader class to read and extract RPM header blobs from BerkeleyDB hash databases.
- Implemented methods to detect BerkeleyDB format and extract values, including handling of page sizes and magic numbers.
- Added tests for BerkeleyDbReader to ensure correct functionality and header extraction.

feat: Add Yarn PnP data tests

- Created YarnPnpDataTests to validate package resolution and data loading from Yarn PnP cache.
- Implemented tests for resolved keys, package presence, and loading from cache structure.

test: Add egg-info package fixtures for Python tests

- Created egg-info package fixtures for testing Python analyzers.
- Included PKG-INFO, entry_points.txt, and installed-files.txt for comprehensive coverage.

test: Enhance RPM database reader tests

- Added tests for RpmDatabaseReader to validate fallback to legacy packages when SQLite is missing.
- Implemented helper methods to create legacy package files and RPM headers for testing.

test: Implement dual signing tests

- Added DualSignTests to validate secondary signature addition when configured.
- Created stub implementations for crypto providers and key resolvers to facilitate testing.

chore: Update CI script for Playwright Chromium installation

- Modified ci-console-exports.sh to ensure deterministic Chromium binary installation for console exports tests.
- Added checks for Windows compatibility and environment variable setups for Playwright browsers.
This commit is contained in:
StellaOps Bot
2025-12-07 16:24:45 +02:00
parent e3f28a21ab
commit 11597679ed
199 changed files with 9809 additions and 4404 deletions

View File

@@ -20,19 +20,19 @@ public sealed class PostgresDocumentStore : IDocumentStore
_sourceRepository = sourceRepository ?? throw new ArgumentNullException(nameof(sourceRepository));
}
public async Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
public async Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
{
var row = await _repository.FindAsync(id, cancellationToken).ConfigureAwait(false);
return row is null ? null : Map(row);
}
public async Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
public async Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
{
var row = await _repository.FindBySourceAndUriAsync(sourceName, uri, cancellationToken).ConfigureAwait(false);
return row is null ? null : Map(row);
}
public async Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
public async Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
{
// Ensure source exists
var source = await _sourceRepository.GetByKeyAsync(record.SourceName, cancellationToken).ConfigureAwait(false)
@@ -59,7 +59,7 @@ public sealed class PostgresDocumentStore : IDocumentStore
return Map(saved);
}
public async Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
public async Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
{
await _repository.UpdateStatusAsync(id, status, cancellationToken).ConfigureAwait(false);
}

View File

@@ -50,7 +50,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
const string sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at
FROM vuln.advisories
WHERE id = @id
@@ -69,7 +69,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
const string sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at
FROM vuln.advisories
WHERE advisory_key = @advisory_key
@@ -88,7 +88,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
const string sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at
FROM vuln.advisories
WHERE primary_vuln_id = @vuln_id
@@ -107,7 +107,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
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.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
@@ -132,7 +132,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
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.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
@@ -164,7 +164,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
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.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
@@ -196,7 +196,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
var sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at,
ts_rank(search_vector, websearch_to_tsquery('english', @query)) as rank
FROM vuln.advisories
@@ -236,7 +236,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
const string sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at
FROM vuln.advisories
WHERE severity = @severity
@@ -265,7 +265,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
const string sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at
FROM vuln.advisories
WHERE modified_at > @since
@@ -294,7 +294,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
{
const string sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_payload::text,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at
FROM vuln.advisories
WHERE source_id = @source_id
@@ -370,7 +370,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
)
VALUES (
@id, @advisory_key, @primary_vuln_id, @source_id, @title, @summary, @description,
@severity, @published_at, @modified_at, @withdrawn_at, @provenance::jsonb, @raw_payload::jsonb
@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,
@@ -386,7 +386,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
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,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at
""";

View File

@@ -44,6 +44,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
services.AddScoped<MongoAdvisories.IAdvisoryStore, PostgresAdvisoryStore>();
services.AddScoped<IDocumentRepository, DocumentRepository>();
services.AddScoped<MongoContracts.ISourceStateRepository, PostgresSourceStateAdapter>();
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
@@ -81,6 +82,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
services.AddScoped<MongoAdvisories.IAdvisoryStore, PostgresAdvisoryStore>();
services.AddScoped<IDocumentRepository, DocumentRepository>();
services.AddScoped<MongoContracts.ISourceStateRepository, PostgresSourceStateAdapter>();
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
services.AddScoped<IMergeEventRepository, MergeEventRepository>();

View File

@@ -0,0 +1,173 @@
using System;
using System.Text.Json;
using System.Collections.Generic;
using MongoDB.Bson;
using StellaOps.Concelier.Storage.Postgres.Models;
using StellaOps.Concelier.Storage.Postgres.Repositories;
using MongoContracts = StellaOps.Concelier.Storage.Mongo;
namespace StellaOps.Concelier.Storage.Postgres;
/// <summary>
/// Adapter that satisfies the legacy source state contract using PostgreSQL storage.
/// </summary>
public sealed class PostgresSourceStateAdapter : MongoContracts.ISourceStateRepository
{
private readonly ISourceRepository _sourceRepository;
private readonly Repositories.ISourceStateRepository _stateRepository;
private readonly TimeProvider _timeProvider;
public PostgresSourceStateAdapter(
ISourceRepository sourceRepository,
Repositories.ISourceStateRepository stateRepository,
TimeProvider? timeProvider = null)
{
_sourceRepository = sourceRepository ?? throw new ArgumentNullException(nameof(sourceRepository));
_stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<MongoContracts.SourceStateRecord?> TryGetAsync(string sourceName, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(sourceName);
var source = await _sourceRepository.GetByKeyAsync(sourceName, cancellationToken).ConfigureAwait(false);
if (source is null)
{
return null;
}
var state = await _stateRepository.GetBySourceIdAsync(source.Id, cancellationToken).ConfigureAwait(false);
if (state is null)
{
return null;
}
var cursor = string.IsNullOrWhiteSpace(state.Cursor) ? null : BsonDocument.Parse(state.Cursor);
return new MongoContracts.SourceStateRecord(
sourceName,
Enabled: true,
Paused: false,
Cursor: cursor,
LastSuccess: state.LastSuccessAt,
LastFailure: state.LastError is null ? null : state.LastSyncAt,
FailCount: state.ErrorCount,
BackoffUntil: null,
UpdatedAt: state.UpdatedAt,
LastFailureReason: state.LastError);
}
public async Task UpdateCursorAsync(string sourceName, BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(sourceName);
ArgumentNullException.ThrowIfNull(cursor);
var source = await EnsureSourceAsync(sourceName, cancellationToken).ConfigureAwait(false);
var existing = await _stateRepository.GetBySourceIdAsync(source.Id, cancellationToken).ConfigureAwait(false);
var entity = new SourceStateEntity
{
Id = existing?.Id ?? Guid.NewGuid(),
SourceId = source.Id,
Cursor = cursor.ToJson(),
LastSyncAt = completedAt,
LastSuccessAt = completedAt,
LastError = null,
SyncCount = (existing?.SyncCount ?? 0) + 1,
ErrorCount = existing?.ErrorCount ?? 0,
Metadata = existing?.Metadata ?? "{}",
UpdatedAt = completedAt
};
_ = await _stateRepository.UpsertAsync(entity, cancellationToken).ConfigureAwait(false);
}
public async Task MarkFailureAsync(string sourceName, DateTimeOffset now, TimeSpan backoff, string reason, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(sourceName);
var source = await EnsureSourceAsync(sourceName, cancellationToken).ConfigureAwait(false);
var existing = await _stateRepository.GetBySourceIdAsync(source.Id, cancellationToken).ConfigureAwait(false);
var backoffUntil = SafeAdd(now, backoff);
var metadata = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["backoffUntil"] = backoffUntil.ToString("O"),
["reason"] = reason
};
var entity = new SourceStateEntity
{
Id = existing?.Id ?? Guid.NewGuid(),
SourceId = source.Id,
Cursor = existing?.Cursor,
LastSyncAt = now,
LastSuccessAt = existing?.LastSuccessAt,
LastError = reason,
SyncCount = existing?.SyncCount ?? 0,
ErrorCount = (existing?.ErrorCount ?? 0) + 1,
Metadata = JsonSerializer.Serialize(metadata, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
UpdatedAt = now
};
_ = await _stateRepository.UpsertAsync(entity, cancellationToken).ConfigureAwait(false);
}
public async Task UpsertAsync(MongoContracts.SourceStateRecord record, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(record);
var source = await EnsureSourceAsync(record.SourceName, cancellationToken).ConfigureAwait(false);
var entity = new SourceStateEntity
{
Id = Guid.NewGuid(),
SourceId = source.Id,
Cursor = record.Cursor?.ToJson(),
LastSyncAt = record.UpdatedAt,
LastSuccessAt = record.LastSuccess,
LastError = record.LastFailureReason,
SyncCount = record.FailCount,
ErrorCount = record.FailCount,
Metadata = "{}",
UpdatedAt = record.UpdatedAt
};
_ = await _stateRepository.UpsertAsync(entity, cancellationToken).ConfigureAwait(false);
}
private async Task<SourceEntity> EnsureSourceAsync(string sourceName, CancellationToken cancellationToken)
{
var existing = await _sourceRepository.GetByKeyAsync(sourceName, cancellationToken).ConfigureAwait(false);
if (existing is not null)
{
return existing;
}
var now = _timeProvider.GetUtcNow();
return await _sourceRepository.UpsertAsync(new SourceEntity
{
Id = Guid.NewGuid(),
Key = sourceName,
Name = sourceName,
SourceType = sourceName,
Url = null,
Priority = 0,
Enabled = true,
Config = "{}",
Metadata = "{}",
CreatedAt = now,
UpdatedAt = now
}, cancellationToken).ConfigureAwait(false);
}
private static DateTimeOffset SafeAdd(DateTimeOffset value, TimeSpan delta)
{
try
{
return value.Add(delta);
}
catch (ArgumentOutOfRangeException)
{
return delta < TimeSpan.Zero ? DateTimeOffset.MinValue : DateTimeOffset.MaxValue;
}
}
}