save progress

This commit is contained in:
StellaOps Bot
2026-01-03 11:02:24 +02:00
parent ca578801fd
commit 83c37243e0
446 changed files with 22798 additions and 4031 deletions

View File

@@ -37,7 +37,7 @@ internal static class SuseCsafParser
var summary = ExtractSummary(documentElement);
var published = ParseDate(trackingElement, "initial_release_date")
?? ParseDate(trackingElement, "current_release_date")
?? DateTimeOffset.UtcNow;
?? DateTimeOffset.MinValue;
var references = new List<SuseReferenceDto>();
if (documentElement.TryGetProperty("references", out var referencesElement) &&
@@ -217,7 +217,11 @@ internal static class SuseCsafParser
}
if (dateElement.ValueKind == JsonValueKind.String &&
DateTimeOffset.TryParse(dateElement.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed))
DateTimeOffset.TryParse(
dateElement.GetString(),
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var parsed))
{
return parsed.ToUniversalTime();
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StellaOps.Concelier.Documents;
@@ -32,7 +33,11 @@ internal sealed record SuseCursor(
lastModified = lastValue.DocumentType switch
{
DocumentType.DateTime => DateTime.SpecifyKind(lastValue.ToUniversalTime(), DateTimeKind.Utc),
DocumentType.String when DateTimeOffset.TryParse(lastValue.AsString, out var parsed) => parsed.ToUniversalTime(),
DocumentType.String when DateTimeOffset.TryParse(
lastValue.AsString,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var parsed) => parsed.ToUniversalTime(),
_ => null,
};
}
@@ -49,8 +54,8 @@ internal sealed record SuseCursor(
{
var document = new DocumentObject
{
["pendingDocuments"] = new DocumentArray(PendingDocuments.Select(static id => id.ToString())),
["pendingMappings"] = new DocumentArray(PendingMappings.Select(static id => id.ToString())),
["pendingDocuments"] = new DocumentArray(PendingDocuments.OrderBy(static id => id).Select(static id => id.ToString())),
["pendingMappings"] = new DocumentArray(PendingMappings.OrderBy(static id => id).Select(static id => id.ToString())),
};
if (LastModified.HasValue)
@@ -60,13 +65,14 @@ internal sealed record SuseCursor(
if (ProcessedIds.Count > 0)
{
document["processedIds"] = new DocumentArray(ProcessedIds);
document["processedIds"] = new DocumentArray(
ProcessedIds.OrderBy(static id => id, StringComparer.OrdinalIgnoreCase));
}
if (FetchCache.Count > 0)
{
var cacheDocument = new DocumentObject();
foreach (var (key, entry) in FetchCache)
foreach (var (key, entry) in FetchCache.OrderBy(static entry => entry.Key, StringComparer.OrdinalIgnoreCase))
{
cacheDocument[key] = entry.ToDocumentObject();
}
@@ -78,10 +84,10 @@ internal sealed record SuseCursor(
}
public SuseCursor WithPendingDocuments(IEnumerable<Guid> ids)
=> this with { PendingDocuments = ids?.Distinct().ToArray() ?? EmptyGuidList };
=> this with { PendingDocuments = ids?.Distinct().OrderBy(static id => id).ToArray() ?? EmptyGuidList };
public SuseCursor WithPendingMappings(IEnumerable<Guid> ids)
=> this with { PendingMappings = ids?.Distinct().ToArray() ?? EmptyGuidList };
=> this with { PendingMappings = ids?.Distinct().OrderBy(static id => id).ToArray() ?? EmptyGuidList };
public SuseCursor WithFetchCache(IDictionary<string, SuseFetchCacheEntry>? cache)
{
@@ -100,6 +106,7 @@ internal sealed record SuseCursor(
ProcessedIds = ids?.Where(static id => !string.IsNullOrWhiteSpace(id))
.Select(static id => id.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(static id => id, StringComparer.OrdinalIgnoreCase)
.ToArray() ?? EmptyStringList
};

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using StellaOps.Concelier.Documents;
using LegacyContracts = StellaOps.Concelier.Storage;
using StorageContracts = StellaOps.Concelier.Storage.Contracts;
@@ -35,7 +36,11 @@ internal sealed record SuseFetchCacheEntry(string? ETag, DateTimeOffset? LastMod
lastModified = modifiedValue.DocumentType switch
{
DocumentType.DateTime => DateTime.SpecifyKind(modifiedValue.ToUniversalTime(), DateTimeKind.Utc),
DocumentType.String when DateTimeOffset.TryParse(modifiedValue.AsString, out var parsed) => parsed.ToUniversalTime(),
DocumentType.String when DateTimeOffset.TryParse(
modifiedValue.AsString,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var parsed) => parsed.ToUniversalTime(),
_ => null,
};
}

View File

@@ -5,6 +5,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
@@ -31,6 +32,7 @@ public sealed class SuseConnector : IFeedConnector
new EventId(1, "SuseMapped"),
"SUSE advisory {AdvisoryId} mapped with {AffectedCount} affected packages");
private const string DtoSchemaVersion = "suse.csaf.v1";
private readonly SourceFetchService _fetchService;
private readonly RawDocumentStorage _rawDocumentStorage;
private readonly IDocumentStore _documentStore;
@@ -186,6 +188,13 @@ public sealed class SuseConnector : IFeedConnector
{
cancellationToken.ThrowIfCancellationRequested();
if (cursor.LastModified.HasValue
&& record.ModifiedAt == cursor.LastModified.Value
&& processedIds.Contains(record.FileName))
{
continue;
}
var detailUri = new Uri(_options.AdvisoryBaseUri, record.FileName);
var cacheKey = detailUri.AbsoluteUri;
touchedResources.Add(cacheKey);
@@ -237,6 +246,18 @@ public sealed class SuseConnector : IFeedConnector
}
}
if (record.ModifiedAt > maxModified)
{
maxModified = record.ModifiedAt;
processedUpdated = true;
currentWindowIds.Clear();
currentWindowIds.Add(record.FileName);
}
else if (record.ModifiedAt == maxModified)
{
currentWindowIds.Add(record.FileName);
}
continue;
}
@@ -248,13 +269,18 @@ public sealed class SuseConnector : IFeedConnector
fetchCache[cacheKey] = SuseFetchCacheEntry.FromDocument(result.Document);
pendingDocuments.Add(result.Document.Id);
pendingMappings.Remove(result.Document.Id);
currentWindowIds.Add(record.FileName);
if (record.ModifiedAt > maxModified)
{
maxModified = record.ModifiedAt;
processedUpdated = true;
currentWindowIds.Clear();
currentWindowIds.Add(record.FileName);
}
else if (record.ModifiedAt == maxModified)
{
currentWindowIds.Add(record.FileName);
}
}
}
@@ -346,7 +372,14 @@ public sealed class SuseConnector : IFeedConnector
await _documentStore.UpsertAsync(updatedDocument, cancellationToken).ConfigureAwait(false);
var payload = ToDocument(dto);
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "suse.csaf.v1", payload, _timeProvider.GetUtcNow());
var dtoRecord = new DtoRecord(
CreateDeterministicGuid($"suse:dto:{document.Id}:{DtoSchemaVersion}"),
document.Id,
SourceName,
DtoSchemaVersion,
payload,
_timeProvider.GetUtcNow(),
SchemaVersion: DtoSchemaVersion);
await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);
@@ -402,19 +435,28 @@ public sealed class SuseConnector : IFeedConnector
continue;
}
var advisory = SuseMapper.Map(dto, document, _timeProvider.GetUtcNow());
await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
await _documentStore.UpdateStatusAsync(documentId, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
// Ingest to canonical advisory service if available
if (_canonicalService is not null)
try
{
var rawPayloadJson = dtoRecord.Payload.ToJson(new JsonWriterSettings { OutputMode = JsonOutputMode.RelaxedExtendedJson });
await IngestToCanonicalAsync(advisory, rawPayloadJson, document.FetchedAt, cancellationToken).ConfigureAwait(false);
var advisory = SuseMapper.Map(dto, document, _timeProvider.GetUtcNow());
await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
await _documentStore.UpdateStatusAsync(documentId, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
// Ingest to canonical advisory service if available
if (_canonicalService is not null)
{
var rawPayloadJson = dtoRecord.Payload.ToJson(new JsonWriterSettings { OutputMode = JsonOutputMode.RelaxedExtendedJson });
await IngestToCanonicalAsync(advisory, rawPayloadJson, document.FetchedAt, cancellationToken).ConfigureAwait(false);
}
LogMapped(_logger, dto.AdvisoryId, advisory.AffectedPackages.Length, null);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to map SUSE advisory for document {DocumentId}", documentId);
await _documentStore.UpdateStatusAsync(documentId, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
}
pendingMappings.Remove(documentId);
LogMapped(_logger, dto.AdvisoryId, advisory.AffectedPackages.Length, null);
}
var updatedCursor = cursor.WithPendingMappings(pendingMappings);
@@ -511,10 +553,14 @@ public sealed class SuseConnector : IFeedConnector
? publishedValue.DocumentType switch
{
DocumentType.DateTime => DateTime.SpecifyKind(publishedValue.ToUniversalTime(), DateTimeKind.Utc),
DocumentType.String when DateTimeOffset.TryParse(publishedValue.AsString, out var parsed) => parsed.ToUniversalTime(),
_ => DateTimeOffset.UtcNow
DocumentType.String when DateTimeOffset.TryParse(
publishedValue.AsString,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var parsed) => parsed.ToUniversalTime(),
_ => DateTimeOffset.MinValue
}
: DateTimeOffset.UtcNow;
: DateTimeOffset.MinValue;
var cves = document.TryGetValue("cves", out var cveArray) && cveArray is DocumentArray cveArr
? cveArr.OfType<DocumentValue>()
@@ -522,6 +568,7 @@ public sealed class SuseConnector : IFeedConnector
.Where(static value => !string.IsNullOrWhiteSpace(value))
.Select(static value => value!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
.ToArray()
: Array.Empty<string>();
@@ -665,4 +712,12 @@ public sealed class SuseConnector : IFeedConnector
}
}
}
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

@@ -7,5 +7,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0169-M | DONE | Maintainability audit for StellaOps.Concelier.Connector.Distro.Suse. |
| AUDIT-0169-T | DONE | Test coverage audit for StellaOps.Concelier.Connector.Distro.Suse. |
| AUDIT-0169-A | TODO | Pending approval for changes. |
| AUDIT-0169-A | DONE | Applied audit remediations. |
| CICD-VAL-SMOKE-001 | DOING | Smoke validation: trim CSAF product IDs to preserve package mapping. |