save progress
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -24,6 +25,7 @@ namespace StellaOps.Concelier.Connector.CertCc;
|
||||
|
||||
public sealed class CertCcConnector : IFeedConnector
|
||||
{
|
||||
private const string DtoSchemaVersion = "certcc.vince.note.v1";
|
||||
private static readonly JsonSerializerOptions DtoSerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
@@ -155,6 +157,10 @@ public sealed class CertCcConnector : IFeedConnector
|
||||
if (result.IsNotModified)
|
||||
{
|
||||
_diagnostics.SummaryFetchUnchanged(request.Scope);
|
||||
if (existingSummary is not null)
|
||||
{
|
||||
await _documentStore.UpdateStatusAsync(existingSummary.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -166,6 +172,11 @@ public sealed class CertCcConnector : IFeedConnector
|
||||
|
||||
_diagnostics.SummaryFetchSuccess(request.Scope);
|
||||
|
||||
if (result.Document is not null)
|
||||
{
|
||||
await _documentStore.UpdateStatusAsync(result.Document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!shouldProcessNotes)
|
||||
{
|
||||
continue;
|
||||
@@ -345,12 +356,13 @@ public sealed class CertCcConnector : IFeedConnector
|
||||
dto.Vulnerabilities.Count);
|
||||
|
||||
var dtoRecord = new DtoRecord(
|
||||
Guid.NewGuid(),
|
||||
CreateDeterministicGuid($"certcc:dto:{group.Note.Id}:{DtoSchemaVersion}"),
|
||||
group.Note.Id,
|
||||
SourceName,
|
||||
"certcc.vince.note.v1",
|
||||
DtoSchemaVersion,
|
||||
payload,
|
||||
_timeProvider.GetUtcNow());
|
||||
_timeProvider.GetUtcNow(),
|
||||
SchemaVersion: DtoSchemaVersion);
|
||||
|
||||
await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
|
||||
await _documentStore.UpdateStatusAsync(group.Note.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);
|
||||
@@ -785,4 +797,12 @@ public sealed class CertCcConnector : IFeedConnector
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public sealed class CertCcOptions
|
||||
public TimeSpan DetailRequestDelay { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <summary>
|
||||
/// When disabled, parse/map stages skip detail mapping—useful for dry runs or migration staging.
|
||||
/// When disabled, parse/map stages skip detail mapping -- useful for dry runs or migration staging.
|
||||
/// </summary>
|
||||
public bool EnableDetailMapping { get; set; } = true;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using StellaOps.Concelier.Documents;
|
||||
using System.Globalization;
|
||||
using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using StellaOps.Concelier.Documents;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertCc.Internal;
|
||||
|
||||
@@ -30,10 +31,10 @@ internal sealed record CertCcCursor(
|
||||
SummaryState.WriteTo(summary, "start", "end");
|
||||
document["summary"] = summary;
|
||||
|
||||
document["pendingSummaries"] = new DocumentArray(PendingSummaries.Select(static id => id.ToString()));
|
||||
document["pendingNotes"] = new DocumentArray(PendingNotes.Select(static note => note));
|
||||
document["pendingDocuments"] = new DocumentArray(PendingDocuments.Select(static id => id.ToString()));
|
||||
document["pendingMappings"] = new DocumentArray(PendingMappings.Select(static id => id.ToString()));
|
||||
document["pendingSummaries"] = new DocumentArray(PendingSummaries.OrderBy(static id => id).Select(static id => id.ToString()));
|
||||
document["pendingNotes"] = new DocumentArray(PendingNotes.OrderBy(static note => note, StringComparer.OrdinalIgnoreCase));
|
||||
document["pendingDocuments"] = new DocumentArray(PendingDocuments.OrderBy(static id => id).Select(static id => id.ToString()));
|
||||
document["pendingMappings"] = new DocumentArray(PendingMappings.OrderBy(static id => id).Select(static id => id.ToString()));
|
||||
|
||||
if (LastRun.HasValue)
|
||||
{
|
||||
@@ -67,7 +68,11 @@ internal sealed record CertCcCursor(
|
||||
lastRun = lastRunValue.DocumentType switch
|
||||
{
|
||||
DocumentType.DateTime => DateTime.SpecifyKind(lastRunValue.ToUniversalTime(), DateTimeKind.Utc),
|
||||
DocumentType.String when DateTimeOffset.TryParse(lastRunValue.AsString, out var parsed) => parsed.ToUniversalTime(),
|
||||
DocumentType.String when DateTimeOffset.TryParse(
|
||||
lastRunValue.AsString,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out var parsed) => parsed.ToUniversalTime(),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -109,7 +114,7 @@ internal sealed record CertCcCursor(
|
||||
}
|
||||
}
|
||||
|
||||
return results.Count == 0 ? EmptyGuidArray : results.Distinct().ToArray();
|
||||
return results.Count == 0 ? EmptyGuidArray : results.Distinct().OrderBy(static id => id).ToArray();
|
||||
}
|
||||
|
||||
private static string[] ReadStringArray(DocumentObject document, string field)
|
||||
@@ -139,6 +144,7 @@ internal sealed record CertCcCursor(
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
@@ -174,7 +180,7 @@ internal sealed record CertCcCursor(
|
||||
}
|
||||
|
||||
private static Guid[] NormalizeGuidSet(IEnumerable<Guid>? ids)
|
||||
=> ids?.Where(static id => id != Guid.Empty).Distinct().ToArray() ?? EmptyGuidArray;
|
||||
=> ids?.Where(static id => id != Guid.Empty).Distinct().OrderBy(static id => id).ToArray() ?? EmptyGuidArray;
|
||||
|
||||
private static string[] NormalizeStringSet(IEnumerable<string>? values)
|
||||
=> values is null
|
||||
@@ -183,5 +189,6 @@ internal sealed record CertCcCursor(
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertCc.Internal;
|
||||
|
||||
@@ -31,7 +32,7 @@ internal static class CertCcMapper
|
||||
|
||||
var metadata = dto.Metadata ?? CertCcNoteMetadata.Empty;
|
||||
|
||||
var advisoryKey = BuildAdvisoryKey(metadata);
|
||||
var advisoryKey = BuildAdvisoryKey(metadata, document);
|
||||
var title = string.IsNullOrWhiteSpace(metadata.Title) ? advisoryKey : metadata.Title.Trim();
|
||||
var summary = ExtractSummary(metadata);
|
||||
|
||||
@@ -61,12 +62,9 @@ internal static class CertCcMapper
|
||||
provenance);
|
||||
}
|
||||
|
||||
private static string BuildAdvisoryKey(CertCcNoteMetadata metadata)
|
||||
private static string BuildAdvisoryKey(CertCcNoteMetadata metadata, DocumentRecord document)
|
||||
{
|
||||
if (metadata is null)
|
||||
{
|
||||
return $"{AdvisoryPrefix}/{Guid.NewGuid():N}";
|
||||
}
|
||||
var fallbackKey = BuildFallbackKey(document);
|
||||
|
||||
var vuKey = NormalizeVuId(metadata.VuId);
|
||||
if (vuKey.Length > 0)
|
||||
@@ -80,9 +78,30 @@ internal static class CertCcMapper
|
||||
return $"{AdvisoryPrefix}/vu-{id}";
|
||||
}
|
||||
|
||||
return $"{AdvisoryPrefix}/{Guid.NewGuid():N}";
|
||||
return fallbackKey;
|
||||
}
|
||||
|
||||
private static string BuildFallbackKey(DocumentRecord document)
|
||||
{
|
||||
if (document.Metadata is not null
|
||||
&& document.Metadata.TryGetValue("certcc.noteId", out var noteId)
|
||||
&& !string.IsNullOrWhiteSpace(noteId))
|
||||
{
|
||||
var normalized = SanitizeToken(noteId);
|
||||
if (normalized.Length > 0)
|
||||
{
|
||||
return $"{AdvisoryPrefix}/note-{normalized}";
|
||||
}
|
||||
}
|
||||
|
||||
var source = document.Uri ?? string.Empty;
|
||||
var hash = ComputeSha256(source);
|
||||
return $"{AdvisoryPrefix}/doc-{hash}";
|
||||
}
|
||||
|
||||
private static string ComputeSha256(string value)
|
||||
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value ?? string.Empty))).ToLowerInvariant();
|
||||
|
||||
private static string NormalizeVuId(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
||||
@@ -361,7 +361,7 @@ internal static class CertCcNoteParser
|
||||
{
|
||||
if (index > start)
|
||||
{
|
||||
AppendSegment(span, start, index - start, baseUri, buffer, ref count);
|
||||
AppendSegment(span, start, index - start, baseUri, ref buffer, ref count);
|
||||
}
|
||||
|
||||
if (ch == '\r' && index + 1 < span.Length && span[index + 1] == '\n')
|
||||
@@ -375,7 +375,7 @@ internal static class CertCcNoteParser
|
||||
|
||||
if (start < span.Length)
|
||||
{
|
||||
AppendSegment(span, start, span.Length - start, baseUri, buffer, ref count);
|
||||
AppendSegment(span, start, span.Length - start, baseUri, ref buffer, ref count);
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
@@ -395,7 +395,7 @@ internal static class CertCcNoteParser
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendSegment(ReadOnlySpan<char> span, int start, int length, Uri? baseUri, string[] buffer, ref int count)
|
||||
private static void AppendSegment(ReadOnlySpan<char> span, int start, int length, Uri? baseUri, ref string[] buffer, ref int count)
|
||||
{
|
||||
var segment = span.Slice(start, length).ToString().Trim();
|
||||
if (segment.Length == 0)
|
||||
@@ -408,12 +408,28 @@ internal static class CertCcNoteParser
|
||||
return;
|
||||
}
|
||||
|
||||
if (count >= buffer.Length)
|
||||
EnsureCapacity(ref buffer, count + 1);
|
||||
|
||||
buffer[count++] = normalized.ToString();
|
||||
}
|
||||
|
||||
private static void EnsureCapacity(ref string[] buffer, int required)
|
||||
{
|
||||
if (required <= buffer.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
buffer[count++] = normalized.ToString();
|
||||
var nextSize = buffer.Length * 2;
|
||||
if (nextSize < required)
|
||||
{
|
||||
nextSize = required;
|
||||
}
|
||||
|
||||
var next = ArrayPool<string>.Shared.Rent(nextSize);
|
||||
Array.Copy(buffer, next, buffer.Length);
|
||||
ArrayPool<string>.Shared.Return(buffer, clearArray: true);
|
||||
buffer = next;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ExtractCveIds(JsonElement element, string propertyName)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -11,15 +11,14 @@ internal static class CertCcVendorStatementParser
|
||||
{
|
||||
"\t",
|
||||
" - ",
|
||||
" – ",
|
||||
" — ",
|
||||
" -- ",
|
||||
" : ",
|
||||
": ",
|
||||
" :",
|
||||
":",
|
||||
};
|
||||
|
||||
private static readonly char[] BulletPrefixes = { '-', '*', '•', '+', '\t' };
|
||||
private static readonly char[] BulletPrefixes = { '-', '*', '+', '\t' };
|
||||
private static readonly char[] ProductDelimiters = { '/', ',', ';', '&' };
|
||||
|
||||
// Matches dotted numeric versions and simple alphanumeric suffixes (e.g., 4.4.3.6, 3.9.9.12, 10.2a)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0153-M | DONE | Maintainability audit for StellaOps.Concelier.Connector.CertCc. |
|
||||
| AUDIT-0153-T | DONE | Test coverage audit for StellaOps.Concelier.Connector.CertCc. |
|
||||
| AUDIT-0153-A | TODO | Pending approval for changes. |
|
||||
| AUDIT-0153-A | DONE | Determinism and parser fixes applied. |
|
||||
|
||||
Reference in New Issue
Block a user