Rename Concelier Source modules to Connector
This commit is contained in:
		@@ -0,0 +1,118 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using MongoDB.Bson;
 | 
			
		||||
using StellaOps.Concelier.Models;
 | 
			
		||||
using StellaOps.Concelier.Connector.CertCc.Internal;
 | 
			
		||||
using StellaOps.Concelier.Storage.Mongo.Documents;
 | 
			
		||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
 | 
			
		||||
using Xunit;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;
 | 
			
		||||
 | 
			
		||||
public sealed class CertCcMapperTests
 | 
			
		||||
{
 | 
			
		||||
    private static readonly DateTimeOffset PublishedAt = DateTimeOffset.Parse("2025-10-03T11:35:31Z", CultureInfo.InvariantCulture);
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Map_ProducesCanonicalAdvisoryWithVendorPrimitives()
 | 
			
		||||
    {
 | 
			
		||||
        const string vendorStatement =
 | 
			
		||||
            "The issue is confirmed, and here is the patch list\n\n" +
 | 
			
		||||
            "V3912/V3910/V2962/V1000B\t4.4.3.6/4.4.5.1\n" +
 | 
			
		||||
            "V2927/V2865/V2866\t4.5.1\n" +
 | 
			
		||||
            "V2765/V2766/V2763/V2135\t4.5.1";
 | 
			
		||||
 | 
			
		||||
        var vendor = new CertCcVendorDto(
 | 
			
		||||
            "DrayTek Corporation",
 | 
			
		||||
            ContactDate: PublishedAt.AddDays(-10),
 | 
			
		||||
            StatementDate: PublishedAt.AddDays(-5),
 | 
			
		||||
            Updated: PublishedAt,
 | 
			
		||||
            Statement: vendorStatement,
 | 
			
		||||
            Addendum: null,
 | 
			
		||||
            References: new[] { "https://www.draytek.com/support/resources?type=version" });
 | 
			
		||||
 | 
			
		||||
        var vendorStatus = new CertCcVendorStatusDto(
 | 
			
		||||
            Vendor: "DrayTek Corporation",
 | 
			
		||||
            CveId: "CVE-2025-10547",
 | 
			
		||||
            Status: "Affected",
 | 
			
		||||
            Statement: null,
 | 
			
		||||
            References: Array.Empty<string>(),
 | 
			
		||||
            DateAdded: PublishedAt,
 | 
			
		||||
            DateUpdated: PublishedAt);
 | 
			
		||||
 | 
			
		||||
        var vulnerability = new CertCcVulnerabilityDto(
 | 
			
		||||
            CveId: "CVE-2025-10547",
 | 
			
		||||
            Description: null,
 | 
			
		||||
            DateAdded: PublishedAt,
 | 
			
		||||
            DateUpdated: PublishedAt);
 | 
			
		||||
 | 
			
		||||
        var metadata = new CertCcNoteMetadata(
 | 
			
		||||
            VuId: "VU#294418",
 | 
			
		||||
            IdNumber: "294418",
 | 
			
		||||
            Title: "Vigor routers running DrayOS RCE via EasyVPN",
 | 
			
		||||
            Overview: "Overview",
 | 
			
		||||
            Summary: "Summary",
 | 
			
		||||
            Published: PublishedAt,
 | 
			
		||||
            Updated: PublishedAt.AddMinutes(5),
 | 
			
		||||
            Created: PublishedAt,
 | 
			
		||||
            Revision: 2,
 | 
			
		||||
            CveIds: new[] { "CVE-2025-10547" },
 | 
			
		||||
            PublicUrls: new[]
 | 
			
		||||
            {
 | 
			
		||||
                "https://www.draytek.com/about/security-advisory/use-of-uninitialized-variable-vulnerabilities/",
 | 
			
		||||
                "https://www.draytek.com/support/resources?type=version"
 | 
			
		||||
            },
 | 
			
		||||
            PrimaryUrl: "https://www.kb.cert.org/vuls/id/294418/");
 | 
			
		||||
 | 
			
		||||
        var dto = new CertCcNoteDto(
 | 
			
		||||
            metadata,
 | 
			
		||||
            Vendors: new[] { vendor },
 | 
			
		||||
            VendorStatuses: new[] { vendorStatus },
 | 
			
		||||
            Vulnerabilities: new[] { vulnerability });
 | 
			
		||||
 | 
			
		||||
        var document = new DocumentRecord(
 | 
			
		||||
            Guid.NewGuid(),
 | 
			
		||||
            "cert-cc",
 | 
			
		||||
            "https://www.kb.cert.org/vuls/id/294418/",
 | 
			
		||||
            PublishedAt,
 | 
			
		||||
            Sha256: new string('0', 64),
 | 
			
		||||
            Status: "pending-map",
 | 
			
		||||
            ContentType: "application/json",
 | 
			
		||||
            Headers: null,
 | 
			
		||||
            Metadata: null,
 | 
			
		||||
            Etag: null,
 | 
			
		||||
            LastModified: PublishedAt,
 | 
			
		||||
            GridFsId: null);
 | 
			
		||||
 | 
			
		||||
        var dtoRecord = new DtoRecord(
 | 
			
		||||
            Id: Guid.NewGuid(),
 | 
			
		||||
            DocumentId: document.Id,
 | 
			
		||||
            SourceName: "cert-cc",
 | 
			
		||||
            SchemaVersion: "certcc.vince.note.v1",
 | 
			
		||||
            Payload: new BsonDocument(),
 | 
			
		||||
            ValidatedAt: PublishedAt.AddMinutes(1));
 | 
			
		||||
 | 
			
		||||
        var advisory = CertCcMapper.Map(dto, document, dtoRecord, "cert-cc");
 | 
			
		||||
 | 
			
		||||
        Assert.Equal("certcc/vu-294418", advisory.AdvisoryKey);
 | 
			
		||||
        Assert.Contains("VU#294418", advisory.Aliases);
 | 
			
		||||
        Assert.Contains("CVE-2025-10547", advisory.Aliases);
 | 
			
		||||
        Assert.Equal("en", advisory.Language);
 | 
			
		||||
        Assert.Equal(PublishedAt, advisory.Published);
 | 
			
		||||
 | 
			
		||||
        Assert.Contains(advisory.References, reference => reference.Url.Contains("/vuls/id/294418", StringComparison.OrdinalIgnoreCase));
 | 
			
		||||
 | 
			
		||||
        var affected = Assert.Single(advisory.AffectedPackages);
 | 
			
		||||
        Assert.Equal("vendor", affected.Type);
 | 
			
		||||
        Assert.Equal("DrayTek Corporation", affected.Identifier);
 | 
			
		||||
        Assert.Contains(affected.Statuses, status => status.Status == AffectedPackageStatusCatalog.Affected);
 | 
			
		||||
 | 
			
		||||
        var range = Assert.Single(affected.VersionRanges);
 | 
			
		||||
        Assert.NotNull(range.Primitives);
 | 
			
		||||
        Assert.NotNull(range.Primitives!.VendorExtensions);
 | 
			
		||||
        Assert.Contains(range.Primitives.VendorExtensions!, kvp => kvp.Key == "certcc.vendor.patches");
 | 
			
		||||
 | 
			
		||||
        Assert.NotEmpty(affected.NormalizedVersions);
 | 
			
		||||
        Assert.Contains(affected.NormalizedVersions, rule => rule.Scheme == "certcc.vendor" && rule.Value == "4.5.1");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using StellaOps.Concelier.Connector.CertCc.Internal;
 | 
			
		||||
using Xunit;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;
 | 
			
		||||
 | 
			
		||||
public sealed class CertCcSummaryParserTests
 | 
			
		||||
{
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void ParseNotes_ReturnsTokens_FromStringArray()
 | 
			
		||||
    {
 | 
			
		||||
        var payload = Encoding.UTF8.GetBytes("{\"notes\":[\"VU#123456\",\"VU#654321\"]}");
 | 
			
		||||
 | 
			
		||||
        var notes = CertCcSummaryParser.ParseNotes(payload);
 | 
			
		||||
 | 
			
		||||
        Assert.Equal(new[] { "VU#123456", "VU#654321" }, notes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void ParseNotes_DeduplicatesTokens_IgnoringCaseAndWhitespace()
 | 
			
		||||
    {
 | 
			
		||||
        var payload = Encoding.UTF8.GetBytes("{\"notes\":[\"VU#123456\",\"vu#123456\",\" 123456 \"]}");
 | 
			
		||||
 | 
			
		||||
        var notes = CertCcSummaryParser.ParseNotes(payload);
 | 
			
		||||
 | 
			
		||||
        Assert.Single(notes);
 | 
			
		||||
        Assert.Equal("VU#123456", notes[0], ignoreCase: true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void ParseNotes_ReadsTokens_FromObjectEntries()
 | 
			
		||||
    {
 | 
			
		||||
        var payload = Encoding.UTF8.GetBytes("{\"notes\":[{\"id\":\"VU#294418\"},{\"idnumber\":\"257161\"}]}");
 | 
			
		||||
 | 
			
		||||
        var notes = CertCcSummaryParser.ParseNotes(payload);
 | 
			
		||||
 | 
			
		||||
        Assert.Equal(new[] { "VU#294418", "257161" }, notes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void ParseNotes_SupportsArrayRoot()
 | 
			
		||||
    {
 | 
			
		||||
        var payload = Encoding.UTF8.GetBytes("[\"VU#360686\",\"VU#760160\"]");
 | 
			
		||||
 | 
			
		||||
        var notes = CertCcSummaryParser.ParseNotes(payload);
 | 
			
		||||
 | 
			
		||||
        Assert.Equal(new[] { "VU#360686", "VU#760160" }, notes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void ParseNotes_InvalidStructure_Throws()
 | 
			
		||||
    {
 | 
			
		||||
        var payload = Encoding.UTF8.GetBytes("\"invalid\"");
 | 
			
		||||
 | 
			
		||||
        Assert.Throws<JsonException>(() => CertCcSummaryParser.ParseNotes(payload));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,95 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Microsoft.Extensions.Options;
 | 
			
		||||
using StellaOps.Concelier.Connector.CertCc.Configuration;
 | 
			
		||||
using StellaOps.Concelier.Connector.CertCc.Internal;
 | 
			
		||||
using StellaOps.Concelier.Connector.Common.Cursors;
 | 
			
		||||
using Xunit;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;
 | 
			
		||||
 | 
			
		||||
public sealed class CertCcSummaryPlannerTests
 | 
			
		||||
{
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void CreatePlan_UsesInitialBackfillWindow()
 | 
			
		||||
    {
 | 
			
		||||
        var options = Options.Create(new CertCcOptions
 | 
			
		||||
        {
 | 
			
		||||
            SummaryWindow = new TimeWindowCursorOptions
 | 
			
		||||
            {
 | 
			
		||||
                WindowSize = TimeSpan.FromDays(30),
 | 
			
		||||
                Overlap = TimeSpan.FromDays(3),
 | 
			
		||||
                InitialBackfill = TimeSpan.FromDays(120),
 | 
			
		||||
                MinimumWindowSize = TimeSpan.FromDays(1),
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-10T12:00:00Z"));
 | 
			
		||||
        var planner = new CertCcSummaryPlanner(options, timeProvider);
 | 
			
		||||
 | 
			
		||||
        var plan = planner.CreatePlan(state: null);
 | 
			
		||||
 | 
			
		||||
        Assert.Equal(DateTimeOffset.Parse("2025-06-12T12:00:00Z"), plan.Window.Start);
 | 
			
		||||
        Assert.Equal(DateTimeOffset.Parse("2025-07-12T12:00:00Z"), plan.Window.End);
 | 
			
		||||
 | 
			
		||||
        Assert.Equal(3, plan.Requests.Count);
 | 
			
		||||
 | 
			
		||||
        var monthly = plan.Requests.Where(r => r.Scope == CertCcSummaryScope.Monthly).ToArray();
 | 
			
		||||
        Assert.Collection(monthly,
 | 
			
		||||
            request =>
 | 
			
		||||
            {
 | 
			
		||||
                Assert.Equal(2025, request.Year);
 | 
			
		||||
                Assert.Equal(6, request.Month);
 | 
			
		||||
                Assert.Equal("https://www.kb.cert.org/vuls/api/2025/06/summary/", request.Uri.AbsoluteUri);
 | 
			
		||||
            },
 | 
			
		||||
            request =>
 | 
			
		||||
            {
 | 
			
		||||
                Assert.Equal(2025, request.Year);
 | 
			
		||||
                Assert.Equal(7, request.Month);
 | 
			
		||||
                Assert.Equal("https://www.kb.cert.org/vuls/api/2025/07/summary/", request.Uri.AbsoluteUri);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        var yearly = plan.Requests.Where(r => r.Scope == CertCcSummaryScope.Yearly).ToArray();
 | 
			
		||||
        Assert.Single(yearly);
 | 
			
		||||
        Assert.Equal(2025, yearly[0].Year);
 | 
			
		||||
        Assert.Null(yearly[0].Month);
 | 
			
		||||
        Assert.Equal("https://www.kb.cert.org/vuls/api/2025/summary/", yearly[0].Uri.AbsoluteUri);
 | 
			
		||||
 | 
			
		||||
        Assert.Equal(plan.Window.End, plan.NextState.LastWindowEnd);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void CreatePlan_AdvancesWindowRespectingOverlap()
 | 
			
		||||
    {
 | 
			
		||||
        var options = Options.Create(new CertCcOptions
 | 
			
		||||
        {
 | 
			
		||||
            SummaryWindow = new TimeWindowCursorOptions
 | 
			
		||||
            {
 | 
			
		||||
                WindowSize = TimeSpan.FromDays(30),
 | 
			
		||||
                Overlap = TimeSpan.FromDays(10),
 | 
			
		||||
                InitialBackfill = TimeSpan.FromDays(90),
 | 
			
		||||
                MinimumWindowSize = TimeSpan.FromDays(1),
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-12-01T00:00:00Z"));
 | 
			
		||||
        var planner = new CertCcSummaryPlanner(options, timeProvider);
 | 
			
		||||
 | 
			
		||||
        var first = planner.CreatePlan(null);
 | 
			
		||||
        var second = planner.CreatePlan(first.NextState);
 | 
			
		||||
 | 
			
		||||
        Assert.True(second.Window.Start < second.Window.End);
 | 
			
		||||
        Assert.Equal(first.Window.End - options.Value.SummaryWindow.Overlap, second.Window.Start);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private sealed class TestTimeProvider : TimeProvider
 | 
			
		||||
    {
 | 
			
		||||
        private DateTimeOffset _now;
 | 
			
		||||
 | 
			
		||||
        public TestTimeProvider(DateTimeOffset now) => _now = now;
 | 
			
		||||
 | 
			
		||||
        public override DateTimeOffset GetUtcNow() => _now;
 | 
			
		||||
 | 
			
		||||
        public void Advance(TimeSpan delta) => _now = _now.Add(delta);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using StellaOps.Concelier.Connector.CertCc.Internal;
 | 
			
		||||
using Xunit;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;
 | 
			
		||||
 | 
			
		||||
public sealed class CertCcVendorStatementParserTests
 | 
			
		||||
{
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Parse_ReturnsPatchesForTabDelimitedList()
 | 
			
		||||
    {
 | 
			
		||||
        const string statement =
 | 
			
		||||
            "V3912/V3910/V2962/V1000B\t4.4.3.6/4.4.5.1\n" +
 | 
			
		||||
            "V2927/V2865/V2866\t4.5.1\n" +
 | 
			
		||||
            "V2765/V2766/V2763/V2135\t4.5.1";
 | 
			
		||||
 | 
			
		||||
        var patches = CertCcVendorStatementParser.Parse(statement);
 | 
			
		||||
 | 
			
		||||
        Assert.Equal(11, patches.Count);
 | 
			
		||||
        Assert.Contains(patches, patch => patch.Product == "V3912" && patch.Version == "4.4.3.6");
 | 
			
		||||
        Assert.Contains(patches, patch => patch.Product == "V2962" && patch.Version == "4.4.5.1");
 | 
			
		||||
        Assert.Equal(7, patches.Count(patch => patch.Version == "4.5.1"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Parse_ReturnsEmptyWhenStatementMissing()
 | 
			
		||||
    {
 | 
			
		||||
        var patches = CertCcVendorStatementParser.Parse(null);
 | 
			
		||||
        Assert.Empty(patches);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user