more audit work

This commit is contained in:
master
2026-01-08 10:21:51 +02:00
parent 43c02081ef
commit 51cf4bc16c
546 changed files with 36721 additions and 4003 deletions

View File

@@ -1,8 +1,11 @@
# Concelier Analyzer Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0144-A | DONE | Tests for StellaOps.Concelier.Analyzers. |
| AUDIT-0750-M | DONE | Revalidated 2026-01-07 (test project). |
| AUDIT-0750-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0750-A | DONE | Waived (test project; revalidated 2026-01-07). |

View File

@@ -0,0 +1,27 @@
# Concelier Astra Connector Tests Charter
## Mission
Validate Astra connector configuration, plugin registration, and mapping scaffolding with deterministic tests.
## Responsibilities
- Maintain `StellaOps.Concelier.Connector.Astra.Tests`.
- Keep tests deterministic and offline-friendly.
- Surface open work on `TASKS.md`; update statuses (TODO/DOING/DONE/BLOCKED/REVIEW).
## Key Paths
- `AstraConnectorTests.cs`
## Coordination
- Concelier connector owners (StellaOps.Concelier.Connector.Astra).
## Required Reading
- `docs/modules/concelier/architecture.md`
- `docs/modules/concelier/link-not-merge-schema.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both corresponding sprint file and local `TASKS.md`.
- 2. Keep tests deterministic (stable ordering, timestamps, IDs).
- 3. Avoid network in tests; use fixtures and cached payloads.
- 4. Log any cross-module edits in the sprint Execution Log.

View File

@@ -0,0 +1,10 @@
# Concelier Astra Connector Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0751-M | DONE | Revalidated 2026-01-07 (test project). |
| AUDIT-0751-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0751-A | DONE | Waived (test project; revalidated 2026-01-07). |

View File

@@ -1,5 +1,5 @@
{
"advisoryKey": "CIAD-2024-0005",
"advisoryKey": "certin/CIAD-2024-0005",
"affectedPackages": [
{
"type": "ics-vendor",
@@ -33,16 +33,7 @@
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "cert-in",
"kind": "affected",
"value": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
}
]
"provenance": []
}
],
"aliases": [
@@ -81,11 +72,11 @@
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://cert-in.example/advisory/CIAD-2024-0005",
"source": "unknown",
"kind": "unspecified",
"value": null,
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"recordedAt": "1970-01-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "cert-in",
@@ -95,11 +86,11 @@
{
"kind": "reference",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://vendor.example.com/advisories/example-gateway-bulletin",
"source": "unknown",
"kind": "unspecified",
"value": null,
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"recordedAt": "1970-01-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
@@ -109,11 +100,11 @@
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9990",
"source": "unknown",
"kind": "unspecified",
"value": null,
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"recordedAt": "1970-01-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-9990",
@@ -123,11 +114,11 @@
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9991",
"source": "unknown",
"kind": "unspecified",
"value": null,
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"recordedAt": "1970-01-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-9991",

View File

@@ -15,8 +15,8 @@ public sealed class CannedHttpMessageHandlerTests
handler.SetFallback(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
using var client = handler.CreateClient();
var firstResponse = await client.GetAsync(requestUri);
var secondResponse = await client.GetAsync(new Uri("https://example.test/other"));
var firstResponse = await client.GetAsync(requestUri, TestContext.Current.CancellationToken);
var secondResponse = await client.GetAsync(new Uri("https://example.test/other"), TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, secondResponse.StatusCode);
@@ -32,6 +32,6 @@ public sealed class CannedHttpMessageHandlerTests
handler.AddException(HttpMethod.Get, requestUri, new InvalidOperationException("boom"));
using var client = handler.CreateClient();
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync(requestUri));
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync(requestUri, TestContext.Current.CancellationToken));
}
}

View File

@@ -1,35 +1,29 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.InMemoryRunner;
using StellaOps.Concelier.Documents;
using StellaOps.Concelier.InMemoryDriver;
using StellaOps.Aoc;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
using LegacyContracts = StellaOps.Concelier.Storage;
using StorageContracts = StellaOps.Concelier.Storage.Contracts;
namespace StellaOps.Concelier.Connector.Common.Tests;
public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
{
private readonly InMemoryDbRunner _runner;
private readonly IStorageDatabase _database;
private readonly RawDocumentStorage _rawStorage;
private readonly ICryptoHash _hash;
public SourceFetchServiceGuardTests()
{
_runner = InMemoryDbRunner.Start(singleNodeReplSet: true);
var client = new InMemoryClient(_runner.ConnectionString);
_database = client.GetDatabase($"source-fetch-guard-{Guid.NewGuid():N}");
_rawStorage = new RawDocumentStorage();
_hash = CryptoHashFactory.CreateDefault();
}
@@ -41,15 +35,15 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
var handler = new StaticHttpMessageHandler(() => CreateSuccessResponse(responsePayload));
var client = new HttpClient(handler, disposeHandler: false);
var httpClientFactory = new StaticHttpClientFactory(client);
var documentStore = new RecordingDocumentStore();
var documentStore = new RecordingStorageDocumentStore();
var legacyStore = new NoopDocumentStore();
var guard = new RecordingAdvisoryRawWriteGuard();
var jitter = new NoJitterSource();
var httpOptions = new TestOptionsMonitor<StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions>(new StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions());
var storageOptions = Options.Create(new StorageOptions
var storageOptions = Options.Create(new LegacyContracts.StorageOptions
{
ConnectionString = _runner.ConnectionString,
DatabaseName = _database.DatabaseNamespace.DatabaseName,
DefaultTenant = "tenant-default",
});
var linksetMapper = new NoopAdvisoryLinksetMapper();
@@ -57,6 +51,7 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
var service = new SourceFetchService(
httpClientFactory,
_rawStorage,
legacyStore,
documentStore,
NullLogger<SourceFetchService>.Instance,
jitter,
@@ -90,11 +85,11 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
Assert.True(documentStore.UpsertCount > 0);
Assert.Equal("msrc", documentStore.LastRecord!.Metadata!["source.vendor"]);
Assert.Equal("tenant-default", documentStore.LastRecord.Metadata!["tenant"]);
Assert.NotNull(documentStore.LastRecord.PayloadId);
Assert.NotNull(documentStore.LastRecord.Payload);
// verify raw payload stored
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
var count = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
Assert.Equal(1, count);
var rawPayload = await _rawStorage.DownloadAsync(documentStore.LastRecord.PayloadId!.Value, CancellationToken.None);
Assert.Equal(responsePayload, Encoding.UTF8.GetString(rawPayload));
}
[Fact]
@@ -103,15 +98,15 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
var handler = new StaticHttpMessageHandler(() => CreateSuccessResponse("{\"id\":\"CVE-2025-2222\"}"));
var client = new HttpClient(handler, disposeHandler: false);
var httpClientFactory = new StaticHttpClientFactory(client);
var documentStore = new RecordingDocumentStore();
var documentStore = new RecordingStorageDocumentStore();
var legacyStore = new NoopDocumentStore();
var guard = new RecordingAdvisoryRawWriteGuard { ShouldThrow = true };
var jitter = new NoJitterSource();
var httpOptions = new TestOptionsMonitor<StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions>(new StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions());
var storageOptions = Options.Create(new StorageOptions
var storageOptions = Options.Create(new LegacyContracts.StorageOptions
{
ConnectionString = _runner.ConnectionString,
DatabaseName = _database.DatabaseNamespace.DatabaseName,
DefaultTenant = "tenant-default",
});
var linksetMapper = new NoopAdvisoryLinksetMapper();
@@ -119,6 +114,7 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
var service = new SourceFetchService(
httpClientFactory,
_rawStorage,
legacyStore,
documentStore,
NullLogger<SourceFetchService>.Instance,
jitter,
@@ -140,16 +136,14 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
await Assert.ThrowsAsync<ConcelierAocGuardException>(() => service.FetchAsync(request, CancellationToken.None));
Assert.Equal(0, documentStore.UpsertCount);
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
var count = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
Assert.Equal(0, count);
var recordId = CreateDeterministicGuid($"{request.SourceName}:{request.RequestUri}");
await Assert.ThrowsAsync<FileNotFoundException>(() => _rawStorage.DownloadAsync(recordId, CancellationToken.None));
}
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
public ValueTask DisposeAsync()
{
_runner.Dispose();
return ValueTask.CompletedTask;
}
@@ -184,24 +178,59 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
=> Task.FromResult(_responseFactory());
}
private sealed class RecordingDocumentStore : IDocumentStore
private sealed class RecordingStorageDocumentStore : StorageContracts.IStorageDocumentStore
{
public DocumentRecord? LastRecord { get; private set; }
private readonly Dictionary<Guid, StorageContracts.StorageDocument> _byId = new();
private readonly Dictionary<(string Source, string Uri), StorageContracts.StorageDocument> _bySourceUri = new();
public StorageContracts.StorageDocument? LastRecord { get; private set; }
public int UpsertCount { get; private set; }
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
public Task<StorageContracts.StorageDocument?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
{
_bySourceUri.TryGetValue((sourceName, uri), out var record);
return Task.FromResult<StorageContracts.StorageDocument?>(record);
}
public Task<StorageContracts.StorageDocument?> FindAsync(Guid id, CancellationToken cancellationToken)
{
_byId.TryGetValue(id, out var record);
return Task.FromResult<StorageContracts.StorageDocument?>(record);
}
public Task<StorageContracts.StorageDocument> UpsertAsync(StorageContracts.StorageDocument record, CancellationToken cancellationToken)
{
UpsertCount++;
LastRecord = record;
_byId[record.Id] = record;
_bySourceUri[(record.SourceName, record.Uri)] = record;
return Task.FromResult(record);
}
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
=> Task.FromResult<DocumentRecord?>(null);
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
{
if (_byId.TryGetValue(id, out var existing))
{
var updated = existing with { Status = status };
_byId[id] = updated;
_bySourceUri[(updated.SourceName, updated.Uri)] = updated;
LastRecord = updated;
}
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
=> Task.FromResult<DocumentRecord?>(null);
return Task.CompletedTask;
}
}
private sealed class NoopDocumentStore : LegacyContracts.IDocumentStore
{
public Task<LegacyContracts.DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
=> Task.FromResult<LegacyContracts.DocumentRecord?>(null);
public Task<LegacyContracts.DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
=> Task.FromResult<LegacyContracts.DocumentRecord?>(null);
public Task<LegacyContracts.DocumentRecord> UpsertAsync(LegacyContracts.DocumentRecord record, CancellationToken cancellationToken)
=> Task.FromResult(record);
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
=> Task.CompletedTask;
@@ -254,6 +283,14 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
{
public RawLinkset Map(AdvisoryRawDocument document) => new();
}
private static Guid CreateDeterministicGuid(string value)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value ?? string.Empty));
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50);
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);
return new Guid(bytes.AsSpan(0, 16));
}
}

View File

@@ -1,9 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using StellaOps.Concelier.InMemoryRunner;
using StellaOps.Concelier.Documents;
using StellaOps.Concelier.InMemoryDriver;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Concelier.Connector.Common;
@@ -16,9 +13,6 @@ namespace StellaOps.Concelier.Connector.Common.Tests;
public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
{
private readonly InMemoryDbRunner _runner;
private readonly InMemoryClient _client;
private readonly IStorageDatabase _database;
private readonly DocumentStore _documentStore;
private readonly RawDocumentStorage _rawStorage;
private readonly InMemorySourceStateRepository _stateRepository;
@@ -27,10 +21,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
public SourceStateSeedProcessorTests()
{
_runner = InMemoryDbRunner.Start(singleNodeReplSet: true);
_client = new InMemoryClient(_runner.ConnectionString);
_database = _client.GetDatabase($"source-state-seed-{Guid.NewGuid():N}");
_documentStore = new DocumentStore(_database, NullLogger<DocumentStore>.Instance);
_documentStore = new DocumentStore();
_rawStorage = new RawDocumentStorage();
_stateRepository = new InMemorySourceStateRepository();
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 28, 12, 0, 0, TimeSpan.Zero));
@@ -98,16 +89,16 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.NotNull(storedDocument.Metadata);
Assert.Equal("value", storedDocument.Metadata!["test.meta"]);
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
var fileCount = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
Assert.Equal(1, fileCount);
var payload = await _rawStorage.DownloadAsync(storedDocument.PayloadId!.Value, CancellationToken.None);
Assert.Equal("{\"id\":\"ADV-1\"}", Encoding.UTF8.GetString(payload));
var state = await _stateRepository.TryGetAsync("vndr.test", CancellationToken.None);
Assert.NotNull(state);
var stateValue = state!;
Assert.Equal(_timeProvider.GetUtcNow().UtcDateTime, stateValue.LastSuccess);
var cursor = stateValue.Cursor;
Assert.NotNull(stateValue.Cursor);
var cursor = stateValue.Cursor!;
var pendingDocs = cursor["pendingDocuments"].AsDocumentArray.Select(v => Guid.Parse(v.AsString)).ToList();
Assert.Contains(documentId, pendingDocs);
@@ -156,9 +147,8 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
var previousGridId = existingRecord!.PayloadId;
Assert.NotNull(previousGridId);
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
var initialFiles = await filesCollection.Find(FilterDefinition<DocumentObject>.Empty).ToListAsync();
Assert.Single(initialFiles);
var initialPayload = await _rawStorage.DownloadAsync(previousGridId!.Value, CancellationToken.None);
Assert.Equal("{\"id\":\"ADV-2\",\"rev\":1}", Encoding.UTF8.GetString(initialPayload));
var updatedSpecification = new SourceStateSeedSpecification
{
@@ -190,11 +180,9 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.NotNull(refreshedRecord);
Assert.Equal(documentId, refreshedRecord!.Id);
Assert.NotNull(refreshedRecord.PayloadId);
Assert.NotEqual(previousGridId?.ToString(), refreshedRecord.PayloadId?.ToString());
var files = await filesCollection.Find(FilterDefinition<DocumentObject>.Empty).ToListAsync();
Assert.Single(files);
Assert.NotEqual(previousGridId?.ToString(), files[0]["_id"].AsObjectId.ToString());
var updatedPayload = await _rawStorage.DownloadAsync(refreshedRecord.PayloadId!.Value, CancellationToken.None);
Assert.Equal("{\"id\":\"ADV-2\",\"rev\":2}", Encoding.UTF8.GetString(updatedPayload));
}
private SourceStateSeedProcessor CreateProcessor()
@@ -210,8 +198,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
public async ValueTask DisposeAsync()
{
await _client.DropDatabaseAsync(_database.DatabaseNamespace.DatabaseName);
_runner.Dispose();
await Task.CompletedTask;
}
}

View File

@@ -8,3 +8,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| AUDIT-0160-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0160-T | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0160-A | DONE | Waived (test project; revalidated 2026-01-06). |
| AUDIT-0374-T | DONE | Revalidated 2026-01-08 (storage store + raw payload checks). |
| AUDIT-0374-A | DONE | Revalidated 2026-01-08 (storage store + raw payload checks). |

View File

@@ -96,7 +96,7 @@ public sealed class PostgresPatchRepositoryTests : IClassFixture<PostgresTestFix
// Assert
results.Should().NotBeEmpty();
results.First().CveId.Should().Be(cveId);
results.First().Method.Should().NotBe(default);
Enum.IsDefined(results.First().Method.GetType(), results.First().Method).Should().BeTrue();
results.First().FingerprintValue.Should().NotBeNullOrEmpty();
results.First().TargetBinary.Should().NotBeNullOrEmpty();
results.First().Metadata.Should().NotBeNull();

View File

@@ -8,3 +8,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| AUDIT-0234-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0234-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0234-A | DONE | Waived (test project; revalidated 2026-01-07). |
| AUDIT-0411-T | DONE | Revalidated 2026-01-08 (fingerprint method assertion). |
| AUDIT-0411-A | DONE | Revalidated 2026-01-08 (fingerprint method assertion). |

View File

@@ -11,7 +11,13 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Options;
namespace StellaOps.Concelier.WebService.Tests.Fixtures;
@@ -27,7 +33,7 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
public ConcelierApplicationFactory() : this(enableSwagger: true, enableOtel: false) { }
public ConcelierApplicationFactory(bool enableSwagger = true, bool enableOtel = false)
protected ConcelierApplicationFactory(bool enableSwagger = true, bool enableOtel = false)
{
_enableSwagger = enableSwagger;
_enableOtel = enableOtel;
@@ -67,6 +73,12 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
{
services.RemoveAll<ILeaseStore>();
services.AddSingleton<ILeaseStore, TestLeaseStore>();
services.RemoveAll<IAdvisoryRawRepository>();
services.AddSingleton<IAdvisoryRawRepository, InMemoryAdvisoryRawRepository>();
services.RemoveAll<IAdvisoryLinksetQueryService>();
services.AddSingleton<IAdvisoryLinksetQueryService, StubAdvisoryLinksetQueryService>();
services.RemoveAll<IAdvisoryObservationQueryService>();
services.AddSingleton<IAdvisoryObservationQueryService, StubAdvisoryObservationQueryService>();
services.AddSingleton<ConcelierOptions>(new ConcelierOptions
{
PostgresStorage = new ConcelierOptions.PostgresStorageOptions
@@ -101,4 +113,109 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
});
});
}
private sealed class InMemoryAdvisoryRawRepository : IAdvisoryRawRepository
{
private static readonly DateTimeOffset FixedTimestamp = DateTimeOffset.UnixEpoch;
private readonly object _lock = new();
private readonly List<AdvisoryRawRecord> _records = new();
public Task<AdvisoryRawUpsertResult> UpsertAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var record = new AdvisoryRawRecord(Guid.NewGuid().ToString("D"), document, FixedTimestamp, FixedTimestamp);
lock (_lock)
{
_records.Add(record);
}
return Task.FromResult(new AdvisoryRawUpsertResult(true, record));
}
public Task<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
AdvisoryRawRecord? record;
lock (_lock)
{
record = _records.FirstOrDefault(candidate =>
string.Equals(candidate.Id, id, StringComparison.Ordinal) &&
string.Equals(candidate.Document.Tenant, tenant, StringComparison.OrdinalIgnoreCase));
}
return Task.FromResult(record);
}
public Task<AdvisoryRawQueryResult> QueryAsync(AdvisoryRawQueryOptions options, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
List<AdvisoryRawRecord> records;
lock (_lock)
{
records = _records
.Where(candidate => string.Equals(candidate.Document.Tenant, options.Tenant, StringComparison.OrdinalIgnoreCase))
.Take(options.Limit)
.ToList();
}
return Task.FromResult(new AdvisoryRawQueryResult(records, null, false));
}
public Task<IReadOnlyList<AdvisoryRawRecord>> FindByAdvisoryKeyAsync(
string tenant,
IReadOnlyCollection<string> searchValues,
IReadOnlyCollection<string> sourceVendors,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<IReadOnlyList<AdvisoryRawRecord>>(Array.Empty<AdvisoryRawRecord>());
}
public Task<IReadOnlyList<AdvisoryRawRecord>> ListForVerificationAsync(
string tenant,
DateTimeOffset since,
DateTimeOffset until,
IReadOnlyCollection<string> sourceVendors,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<IReadOnlyList<AdvisoryRawRecord>>(Array.Empty<AdvisoryRawRecord>());
}
}
private sealed class StubAdvisoryLinksetQueryService : IAdvisoryLinksetQueryService
{
public Task<AdvisoryLinksetQueryResult> QueryAsync(AdvisoryLinksetQueryOptions options, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new AdvisoryLinksetQueryResult(ImmutableArray<AdvisoryLinkset>.Empty, null, false));
}
}
private sealed class StubAdvisoryObservationQueryService : IAdvisoryObservationQueryService
{
public ValueTask<AdvisoryObservationQueryResult> QueryAsync(
AdvisoryObservationQueryOptions options,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var emptyLinkset = new AdvisoryObservationLinksetAggregate(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<AdvisoryObservationReference>.Empty);
return ValueTask.FromResult(new AdvisoryObservationQueryResult(
ImmutableArray<AdvisoryObservation>.Empty,
emptyLinkset,
null,
false));
}
}
}

View File

@@ -326,6 +326,14 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
IsReachable = true,
ScannedAt = DateTimeOffset.UtcNow
});
var scoringService = Services.GetRequiredService<IInterestScoringService>();
scoringService.RecordSbomMatchAsync(
canonicalId,
"sha256:test123",
"pkg:npm/lodash@4.17.21",
isReachable: true,
isDeployed: false).GetAwaiter().GetResult();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)

View File

@@ -5,9 +5,16 @@
// Description: Authorization tests for Concelier.WebService (deny-by-default, token expiry, scope enforcement)
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Net;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Auth.Abstractions;
using StellaOps.Concelier.WebService.Tests.Fixtures;
using StellaOps.Concelier.WebService.Options;
using StellaOps.TestKit;
using Xunit;
@@ -19,11 +26,11 @@ namespace StellaOps.Concelier.WebService.Tests.Security;
/// </summary>
[Trait("Category", TestCategories.Security)]
[Collection("ConcelierWebService")]
public sealed class ConcelierAuthorizationTests : IClassFixture<ConcelierApplicationFactory>
public sealed class ConcelierAuthorizationTests : IClassFixture<ConcelierAuthorizationFactory>
{
private readonly ConcelierApplicationFactory _factory;
private readonly ConcelierAuthorizationFactory _factory;
public ConcelierAuthorizationTests(ConcelierApplicationFactory factory)
public ConcelierAuthorizationTests(ConcelierAuthorizationFactory factory)
{
_factory = factory;
}
@@ -266,3 +273,77 @@ public sealed class ConcelierAuthorizationTests : IClassFixture<ConcelierApplica
#endregion
}
public sealed class ConcelierAuthorizationFactory : ConcelierApplicationFactory
{
private const string TestIssuer = "https://authority.test";
private const string TestSigningSecret = "test-signing-secret";
private readonly string? _previousAuthorityEnabled;
private readonly string? _previousAllowAnonymousFallback;
private readonly string? _previousAuthorityIssuer;
private readonly string? _previousRequireHttps;
private readonly string? _previousSigningSecret;
public ConcelierAuthorizationFactory() : base(enableSwagger: true, enableOtel: false)
{
_previousAuthorityEnabled = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED");
_previousAllowAnonymousFallback = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK");
_previousAuthorityIssuer = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER");
_previousRequireHttps = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA");
_previousSigningSecret = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET");
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED", "true");
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK", "false");
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER", TestIssuer);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA", "false");
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET", TestSigningSecret);
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureAppConfiguration((_, config) =>
{
var overrides = new Dictionary<string, string?>
{
["Authority:Enabled"] = "true",
["Authority:AllowAnonymousFallback"] = "false",
["Authority:Issuer"] = TestIssuer,
["Authority:RequireHttpsMetadata"] = "false",
["Authority:TestSigningSecret"] = TestSigningSecret
};
config.AddInMemoryCollection(overrides);
});
builder.ConfigureServices(services =>
{
services.PostConfigure<ConcelierOptions>(options =>
{
options.Authority ??= new ConcelierOptions.AuthorityOptions();
options.Authority.Enabled = true;
options.Authority.AllowAnonymousFallback = false;
options.Authority.Issuer = TestIssuer;
options.Authority.RequireHttpsMetadata = false;
options.Authority.TestSigningSecret = TestSigningSecret;
options.Authority.RequiredScopes.Clear();
options.Authority.RequiredScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
options.Authority.ClientScopes.Clear();
options.Authority.ClientScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
});
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED", _previousAuthorityEnabled);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK", _previousAllowAnonymousFallback);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER", _previousAuthorityIssuer);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA", _previousRequireHttps);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET", _previousSigningSecret);
}
}

View File

@@ -25,4 +25,9 @@
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
<ItemGroup>
<None Include="Contract\\Expected\\concelier-openapi.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>