feat: Add DigestUpsertRequest and LockEntity models
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
Export Center CI / export-ci (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

- Introduced DigestUpsertRequest for handling digest upsert requests with properties like ChannelId, Recipient, DigestKey, Events, and CollectUntil.
- Created LockEntity to represent a lightweight distributed lock entry with properties such as Id, TenantId, Resource, Owner, ExpiresAt, and CreatedAt.

feat: Implement ILockRepository interface and LockRepository class

- Defined ILockRepository interface with methods for acquiring and releasing locks.
- Implemented LockRepository class with methods to try acquiring a lock and releasing it, using SQL for upsert operations.

feat: Add SurfaceManifestPointer record for manifest pointers

- Introduced SurfaceManifestPointer to represent a minimal pointer to a Surface.FS manifest associated with an image digest.

feat: Create PolicySimulationInputLock and related validation logic

- Added PolicySimulationInputLock record to describe policy simulation inputs and expected digests.
- Implemented validation logic for policy simulation inputs, including checks for digest drift and shadow mode requirements.

test: Add unit tests for ReplayVerificationService and ReplayVerifier

- Created ReplayVerificationServiceTests to validate the behavior of the ReplayVerificationService under various scenarios.
- Developed ReplayVerifierTests to ensure the correctness of the ReplayVerifier logic.

test: Implement PolicySimulationInputLockValidatorTests

- Added tests for PolicySimulationInputLockValidator to verify the validation logic against expected inputs and conditions.

chore: Add cosign key example and signing scripts

- Included a placeholder cosign key example for development purposes.
- Added a script for signing Signals artifacts using cosign with support for both v2 and v3.

chore: Create script for uploading evidence to the evidence locker

- Developed a script to upload evidence to the evidence locker, ensuring required environment variables are set.
This commit is contained in:
StellaOps Bot
2025-12-03 07:51:50 +02:00
parent 37cba83708
commit e923880694
171 changed files with 6567 additions and 2952 deletions

View File

@@ -0,0 +1,30 @@
# Concelier Storage.Postgres — Agent Charter
## Mission & Scope
- Working directory: `src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres`.
- Deliver the PostgreSQL storage layer for Concelier vulnerability data (sources, advisories, aliases/CVSS/affected, KEV, states, snapshots, merge audit).
- Keep behaviour deterministic, air-gap friendly, and aligned with the Link-Not-Merge (LNM) contract: ingest facts, dont derive.
## Roles
- **Backend engineer (.NET 10/Postgres):** repositories, migrations, connection plumbing, perf indexes.
- **QA engineer:** integration tests using Testcontainers PostgreSQL, determinism and replacement semantics on child tables.
## Required Reading (treat as read before DOING)
- `docs/README.md`, `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/concelier/architecture.md`
- `docs/modules/concelier/link-not-merge-schema.md`
- `docs/db/README.md`, `docs/db/SPECIFICATION.md` (Section 5.2), `docs/db/RULES.md`
- Sprint doc: `docs/implplan/SPRINT_3405_0001_0001_postgres_vulnerabilities.md`
## Working Agreements
- Determinism: stable ordering (ORDER BY in queries/tests), UTC timestamps, no random seeds; JSON kept canonical.
- Offline-first: no network in code/tests; fixtures must be self-contained.
- Tenant safety: vulnerability data is global; still pass `_system` tenant id through RepositoryBase; no caller-specific state.
- Schema changes: update migration SQL and docs; keep search/vector triggers intact.
- Status discipline: mirror `TODO → DOING → DONE/BLOCKED` in sprint docs when you start/finish/block tasks.
## Testing Rules
- Use `ConcelierPostgresFixture` (Testcontainers PostgreSQL). Docker daemon must be available.
- Before each test, truncate tables via fixture; avoid cross-test coupling.
- Cover replacement semantics for child tables (aliases/CVSS/affected/etc.), search, PURL lookups, and source state cursor updates.

View File

@@ -0,0 +1,40 @@
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Postgres.Models;
using StellaOps.Concelier.Storage.Postgres.Repositories;
namespace StellaOps.Concelier.Storage.Postgres.Converters;
/// <summary>
/// Service to convert Mongo advisory documents and persist them into PostgreSQL.
/// </summary>
public sealed class AdvisoryConversionService
{
private readonly IAdvisoryRepository _advisories;
public AdvisoryConversionService(IAdvisoryRepository advisories)
{
_advisories = advisories;
}
/// <summary>
/// Converts a Mongo advisory document and persists it (upsert) with all child rows.
/// </summary>
public Task<AdvisoryEntity> ConvertAndUpsertAsync(
AdvisoryDocument doc,
string sourceKey,
Guid sourceId,
CancellationToken cancellationToken = default)
{
var result = AdvisoryConverter.Convert(doc, sourceKey, sourceId);
return _advisories.UpsertAsync(
result.Advisory,
result.Aliases,
result.Cvss,
result.Affected,
result.References,
result.Credits,
result.Weaknesses,
result.KevFlags,
cancellationToken);
}
}

View File

@@ -0,0 +1,297 @@
using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Postgres.Models;
namespace StellaOps.Concelier.Storage.Postgres.Converters;
/// <summary>
/// Converts Mongo advisory documents to Postgres advisory entities and child collections.
/// Deterministic: ordering of child collections is preserved (sorted for stable SQL writes).
/// </summary>
public static class AdvisoryConverter
{
public sealed record Result(
AdvisoryEntity Advisory,
IReadOnlyList<AdvisoryAliasEntity> Aliases,
IReadOnlyList<AdvisoryCvssEntity> Cvss,
IReadOnlyList<AdvisoryAffectedEntity> Affected,
IReadOnlyList<AdvisoryReferenceEntity> References,
IReadOnlyList<AdvisoryCreditEntity> Credits,
IReadOnlyList<AdvisoryWeaknessEntity> Weaknesses,
IReadOnlyList<KevFlagEntity> KevFlags);
/// <summary>
/// Maps a Mongo AdvisoryDocument and its raw payload into Postgres entities.
/// </summary>
public static Result Convert(
AdvisoryDocument doc,
string sourceKey,
Guid sourceId,
string? contentHash = null)
{
var now = DateTimeOffset.UtcNow;
// Top-level advisory
var advisoryId = Guid.NewGuid();
var payloadJson = doc.Payload.ToJson();
var provenanceJson = JsonSerializer.Serialize(new { source = sourceKey });
var advisory = new AdvisoryEntity
{
Id = advisoryId,
AdvisoryKey = doc.AdvisoryKey,
PrimaryVulnId = doc.Payload.GetValue("primaryVulnId", doc.AdvisoryKey)?.ToString() ?? doc.AdvisoryKey,
SourceId = sourceId,
Title = doc.Payload.GetValue("title", null)?.ToString(),
Summary = doc.Payload.GetValue("summary", null)?.ToString(),
Description = doc.Payload.GetValue("description", null)?.ToString(),
Severity = doc.Payload.GetValue("severity", null)?.ToString(),
PublishedAt = doc.Published.HasValue ? DateTime.SpecifyKind(doc.Published.Value, DateTimeKind.Utc) : null,
ModifiedAt = DateTime.SpecifyKind(doc.Modified, DateTimeKind.Utc),
WithdrawnAt = doc.Payload.TryGetValue("withdrawnAt", out var withdrawn) && withdrawn.IsValidDateTime
? withdrawn.ToUniversalTime()
: null,
Provenance = provenanceJson,
RawPayload = payloadJson,
CreatedAt = now,
UpdatedAt = now
};
// Aliases
var aliases = doc.Payload.TryGetValue("aliases", out var aliasesBson) && aliasesBson.IsBsonArray
? aliasesBson.AsBsonArray.Select(v => v.ToString() ?? string.Empty)
: Enumerable.Empty<string>();
var aliasEntities = aliases
.Where(a => !string.IsNullOrWhiteSpace(a))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(a => a, StringComparer.OrdinalIgnoreCase)
.Select((alias, idx) => new AdvisoryAliasEntity
{
Id = Guid.NewGuid(),
AdvisoryId = advisoryId,
AliasType = alias.StartsWith("CVE-", StringComparison.OrdinalIgnoreCase) ? "CVE" : "OTHER",
AliasValue = alias,
IsPrimary = idx == 0,
CreatedAt = now
})
.ToArray();
// CVSS
var cvssEntities = BuildCvssEntities(doc, advisoryId, now);
// Affected
var affectedEntities = BuildAffectedEntities(doc, advisoryId, now);
// References
var referencesEntities = BuildReferenceEntities(doc, advisoryId, now);
// Credits
var creditEntities = BuildCreditEntities(doc, advisoryId, now);
// Weaknesses
var weaknessEntities = BuildWeaknessEntities(doc, advisoryId, now);
// KEV flags (from payload.kev if present)
var kevEntities = BuildKevEntities(doc, advisoryId, now);
return new Result(
advisory,
aliasEntities,
cvssEntities,
affectedEntities,
referencesEntities,
creditEntities,
weaknessEntities,
kevEntities);
}
private static IReadOnlyList<AdvisoryCvssEntity> BuildCvssEntities(AdvisoryDocument doc, Guid advisoryId, DateTimeOffset now)
{
if (!doc.Payload.TryGetValue("cvss", out var cvssValue) || !cvssValue.IsBsonArray)
{
return Array.Empty<AdvisoryCvssEntity>();
}
return cvssValue.AsBsonArray
.Where(v => v.IsBsonDocument)
.Select(v => v.AsBsonDocument)
.Select(d => new AdvisoryCvssEntity
{
Id = Guid.NewGuid(),
AdvisoryId = advisoryId,
CvssVersion = d.GetValue("version", "3.1").ToString() ?? "3.1",
VectorString = d.GetValue("vector", string.Empty).ToString() ?? string.Empty,
BaseScore = d.GetValue("baseScore", 0m).ToDecimal(),
BaseSeverity = d.GetValue("baseSeverity", null)?.ToString(),
ExploitabilityScore = d.GetValue("exploitabilityScore", null)?.ToNullableDecimal(),
ImpactScore = d.GetValue("impactScore", null)?.ToNullableDecimal(),
Source = d.GetValue("source", null)?.ToString(),
IsPrimary = d.GetValue("isPrimary", false).ToBoolean(),
CreatedAt = now
})
.OrderByDescending(c => c.IsPrimary)
.ThenByDescending(c => c.BaseScore)
.ThenBy(c => c.Id)
.ToArray();
}
private static IReadOnlyList<AdvisoryAffectedEntity> BuildAffectedEntities(AdvisoryDocument doc, Guid advisoryId, DateTimeOffset now)
{
if (!doc.Payload.TryGetValue("affected", out var affectedValue) || !affectedValue.IsBsonArray)
{
return Array.Empty<AdvisoryAffectedEntity>();
}
return affectedValue.AsBsonArray
.Where(v => v.IsBsonDocument)
.Select(v => v.AsBsonDocument)
.Select(d => new AdvisoryAffectedEntity
{
Id = Guid.NewGuid(),
AdvisoryId = advisoryId,
Ecosystem = d.GetValue("ecosystem", string.Empty).ToString() ?? string.Empty,
PackageName = d.GetValue("packageName", string.Empty).ToString() ?? string.Empty,
Purl = d.GetValue("purl", null)?.ToString(),
VersionRange = d.GetValue("range", "{}").ToString() ?? "{}",
VersionsAffected = d.TryGetValue("versionsAffected", out var va) && va.IsBsonArray
? va.AsBsonArray.Select(x => x.ToString() ?? string.Empty).ToArray()
: null,
VersionsFixed = d.TryGetValue("versionsFixed", out var vf) && vf.IsBsonArray
? vf.AsBsonArray.Select(x => x.ToString() ?? string.Empty).ToArray()
: null,
DatabaseSpecific = d.GetValue("databaseSpecific", null)?.ToString(),
CreatedAt = now
})
.OrderBy(a => a.Ecosystem)
.ThenBy(a => a.PackageName)
.ThenBy(a => a.Purl)
.ToArray();
}
private static IReadOnlyList<AdvisoryReferenceEntity> BuildReferenceEntities(AdvisoryDocument doc, Guid advisoryId, DateTimeOffset now)
{
if (!doc.Payload.TryGetValue("references", out var referencesValue) || !referencesValue.IsBsonArray)
{
return Array.Empty<AdvisoryReferenceEntity>();
}
return referencesValue.AsBsonArray
.Where(v => v.IsBsonDocument)
.Select(v => v.AsBsonDocument)
.Select(r => new AdvisoryReferenceEntity
{
Id = Guid.NewGuid(),
AdvisoryId = advisoryId,
RefType = r.GetValue("type", "advisory").ToString() ?? "advisory",
Url = r.GetValue("url", string.Empty).ToString() ?? string.Empty,
CreatedAt = now
})
.OrderBy(r => r.Url)
.ToArray();
}
private static IReadOnlyList<AdvisoryCreditEntity> BuildCreditEntities(AdvisoryDocument doc, Guid advisoryId, DateTimeOffset now)
{
if (!doc.Payload.TryGetValue("credits", out var creditsValue) || !creditsValue.IsBsonArray)
{
return Array.Empty<AdvisoryCreditEntity>();
}
return creditsValue.AsBsonArray
.Where(v => v.IsBsonDocument)
.Select(v => v.AsBsonDocument)
.Select(c => new AdvisoryCreditEntity
{
Id = Guid.NewGuid(),
AdvisoryId = advisoryId,
Name = c.GetValue("name", string.Empty).ToString() ?? string.Empty,
Contact = c.GetValue("contact", null)?.ToString(),
CreditType = c.GetValue("type", null)?.ToString(),
CreatedAt = now
})
.OrderBy(c => c.Name)
.ThenBy(c => c.Contact)
.ToArray();
}
private static IReadOnlyList<AdvisoryWeaknessEntity> BuildWeaknessEntities(AdvisoryDocument doc, Guid advisoryId, DateTimeOffset now)
{
if (!doc.Payload.TryGetValue("weaknesses", out var weaknessesValue) || !weaknessesValue.IsBsonArray)
{
return Array.Empty<AdvisoryWeaknessEntity>();
}
return weaknessesValue.AsBsonArray
.Where(v => v.IsBsonDocument)
.Select(v => v.AsBsonDocument)
.Select(w => new AdvisoryWeaknessEntity
{
Id = Guid.NewGuid(),
AdvisoryId = advisoryId,
CweId = w.GetValue("cweId", string.Empty).ToString() ?? string.Empty,
Description = w.GetValue("description", null)?.ToString(),
Source = w.GetValue("source", null)?.ToString(),
CreatedAt = now
})
.OrderBy(w => w.CweId)
.ToArray();
}
private static IReadOnlyList<KevFlagEntity> BuildKevEntities(AdvisoryDocument doc, Guid advisoryId, DateTimeOffset now)
{
if (!doc.Payload.TryGetValue("kev", out var kevValue) || !kevValue.IsBsonArray)
{
return Array.Empty<KevFlagEntity>();
}
var today = DateOnly.FromDateTime(now.UtcDateTime);
return kevValue.AsBsonArray
.Where(v => v.IsBsonDocument)
.Select(v => v.AsBsonDocument)
.Select(k => new KevFlagEntity
{
Id = Guid.NewGuid(),
AdvisoryId = advisoryId,
CveId = k.GetValue("cveId", string.Empty).ToString() ?? string.Empty,
VendorProject = k.GetValue("vendorProject", null)?.ToString(),
Product = k.GetValue("product", null)?.ToString(),
VulnerabilityName = k.GetValue("name", null)?.ToString(),
DateAdded = k.TryGetValue("dateAdded", out var dateAdded) && dateAdded.IsValidDateTime
? DateOnly.FromDateTime(dateAdded.ToUniversalTime().Date)
: today,
DueDate = k.TryGetValue("dueDate", out var dueDate) && dueDate.IsValidDateTime
? DateOnly.FromDateTime(dueDate.ToUniversalTime().Date)
: null,
KnownRansomwareUse = k.GetValue("knownRansomwareUse", false).ToBoolean(),
Notes = k.GetValue("notes", null)?.ToString(),
CreatedAt = now
})
.OrderBy(k => k.CveId)
.ToArray();
}
private static decimal ToDecimal(this object value)
=> value switch
{
decimal d => d,
double d => (decimal)d,
float f => (decimal)f,
IConvertible c => c.ToDecimal(null),
_ => 0m
};
private static decimal? ToNullableDecimal(this object? value)
{
if (value is null) return null;
return value switch
{
decimal d => d,
double d => (decimal)d,
float f => (decimal)f,
IConvertible c => c.ToDecimal(null),
_ => null
};
}
}

View File

@@ -0,0 +1,66 @@
using MongoDB.Driver;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Postgres.Models;
using StellaOps.Concelier.Storage.Postgres.Repositories;
namespace StellaOps.Concelier.Storage.Postgres.Converters.Importers;
/// <summary>
/// Imports GHSA/vendor advisories from Mongo into PostgreSQL.
/// </summary>
public sealed class GhsaImporter
{
private readonly IMongoCollection<AdvisoryDocument> _collection;
private readonly AdvisoryConversionService _conversionService;
private readonly IFeedSnapshotRepository _feedSnapshots;
private readonly IAdvisorySnapshotRepository _advisorySnapshots;
public GhsaImporter(
IMongoCollection<AdvisoryDocument> collection,
AdvisoryConversionService conversionService,
IFeedSnapshotRepository feedSnapshots,
IAdvisorySnapshotRepository advisorySnapshots)
{
_collection = collection;
_conversionService = conversionService;
_feedSnapshots = feedSnapshots;
_advisorySnapshots = advisorySnapshots;
}
public async Task ImportSnapshotAsync(
Guid sourceId,
string sourceKey,
string snapshotId,
CancellationToken cancellationToken)
{
var advisories = await _collection
.Find(Builders<AdvisoryDocument>.Filter.Empty)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var feedSnapshot = await _feedSnapshots.InsertAsync(new FeedSnapshotEntity
{
Id = Guid.NewGuid(),
SourceId = sourceId,
SnapshotId = snapshotId,
AdvisoryCount = advisories.Count,
Metadata = $"{{\"source\":\"{sourceKey}\"}}",
CreatedAt = DateTimeOffset.UtcNow
}, cancellationToken).ConfigureAwait(false);
foreach (var advisory in advisories)
{
var stored = await _conversionService.ConvertAndUpsertAsync(advisory, sourceKey, sourceId, cancellationToken)
.ConfigureAwait(false);
await _advisorySnapshots.InsertAsync(new AdvisorySnapshotEntity
{
Id = Guid.NewGuid(),
FeedSnapshotId = feedSnapshot.Id,
AdvisoryKey = stored.AdvisoryKey,
ContentHash = advisory.Payload.GetValue("hash", advisory.AdvisoryKey)?.ToString() ?? advisory.AdvisoryKey,
CreatedAt = DateTimeOffset.UtcNow
}, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,68 @@
using System.Text.Json;
using MongoDB.Driver;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Postgres.Models;
using StellaOps.Concelier.Storage.Postgres.Repositories;
namespace StellaOps.Concelier.Storage.Postgres.Converters.Importers;
/// <summary>
/// Imports NVD advisory documents from Mongo into PostgreSQL using the advisory converter.
/// </summary>
public sealed class NvdImporter
{
private readonly IMongoCollection<AdvisoryDocument> _collection;
private readonly AdvisoryConversionService _conversionService;
private readonly IFeedSnapshotRepository _feedSnapshots;
private readonly IAdvisorySnapshotRepository _advisorySnapshots;
public NvdImporter(
IMongoCollection<AdvisoryDocument> collection,
AdvisoryConversionService conversionService,
IFeedSnapshotRepository feedSnapshots,
IAdvisorySnapshotRepository advisorySnapshots)
{
_collection = collection;
_conversionService = conversionService;
_feedSnapshots = feedSnapshots;
_advisorySnapshots = advisorySnapshots;
}
public async Task ImportSnapshotAsync(
Guid sourceId,
string sourceKey,
string snapshotId,
CancellationToken cancellationToken)
{
var advisories = await _collection
.Find(Builders<AdvisoryDocument>.Filter.Empty)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var feedSnapshot = await _feedSnapshots.InsertAsync(new FeedSnapshotEntity
{
Id = Guid.NewGuid(),
SourceId = sourceId,
SnapshotId = snapshotId,
AdvisoryCount = advisories.Count,
Checksum = null,
Metadata = JsonSerializer.Serialize(new { source = sourceKey, snapshot = snapshotId }),
CreatedAt = DateTimeOffset.UtcNow
}, cancellationToken).ConfigureAwait(false);
foreach (var advisory in advisories)
{
var stored = await _conversionService.ConvertAndUpsertAsync(advisory, sourceKey, sourceId, cancellationToken)
.ConfigureAwait(false);
await _advisorySnapshots.InsertAsync(new AdvisorySnapshotEntity
{
Id = Guid.NewGuid(),
FeedSnapshotId = feedSnapshot.Id,
AdvisoryKey = stored.AdvisoryKey,
ContentHash = advisory.Payload.GetValue("hash", advisory.AdvisoryKey)?.ToString() ?? advisory.AdvisoryKey,
CreatedAt = DateTimeOffset.UtcNow
}, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,65 @@
using MongoDB.Driver;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Postgres.Models;
using StellaOps.Concelier.Storage.Postgres.Repositories;
namespace StellaOps.Concelier.Storage.Postgres.Converters.Importers;
/// <summary>
/// Imports OSV advisories from Mongo into PostgreSQL.
/// </summary>
public sealed class OsvImporter
{
private readonly IMongoCollection<AdvisoryDocument> _collection;
private readonly AdvisoryConversionService _conversionService;
private readonly IFeedSnapshotRepository _feedSnapshots;
private readonly IAdvisorySnapshotRepository _advisorySnapshots;
public OsvImporter(
IMongoCollection<AdvisoryDocument> collection,
AdvisoryConversionService conversionService,
IFeedSnapshotRepository feedSnapshots,
IAdvisorySnapshotRepository advisorySnapshots)
{
_collection = collection;
_conversionService = conversionService;
_feedSnapshots = feedSnapshots;
_advisorySnapshots = advisorySnapshots;
}
public async Task ImportSnapshotAsync(
Guid sourceId,
string snapshotId,
CancellationToken cancellationToken)
{
var advisories = await _collection
.Find(Builders<AdvisoryDocument>.Filter.Empty)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var feedSnapshot = await _feedSnapshots.InsertAsync(new FeedSnapshotEntity
{
Id = Guid.NewGuid(),
SourceId = sourceId,
SnapshotId = snapshotId,
AdvisoryCount = advisories.Count,
Metadata = "{\"source\":\"osv\"}",
CreatedAt = DateTimeOffset.UtcNow
}, cancellationToken).ConfigureAwait(false);
foreach (var advisory in advisories)
{
var stored = await _conversionService.ConvertAndUpsertAsync(advisory, "osv", sourceId, cancellationToken)
.ConfigureAwait(false);
await _advisorySnapshots.InsertAsync(new AdvisorySnapshotEntity
{
Id = Guid.NewGuid(),
FeedSnapshotId = feedSnapshot.Id,
AdvisoryKey = stored.AdvisoryKey,
ContentHash = advisory.Payload.GetValue("hash", advisory.AdvisoryKey)?.ToString() ?? advisory.AdvisoryKey,
CreatedAt = DateTimeOffset.UtcNow
}, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -16,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="..\StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj" />
</ItemGroup>
</Project>