up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -4,7 +4,7 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Connector.Acsc;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
@@ -13,9 +13,9 @@ using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class AcscConnectorFetchTests
|
||||
{
|
||||
@@ -51,24 +51,24 @@ public sealed class AcscConnectorFetchTests
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString);
|
||||
|
||||
var feeds = state.Cursor.GetValue("feeds").AsBsonDocument;
|
||||
Assert.True(feeds.TryGetValue("alerts", out var published));
|
||||
Assert.Equal(DateTime.Parse("2025-10-11T05:30:00Z").ToUniversalTime(), published.ToUniversalTime());
|
||||
|
||||
var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray;
|
||||
|
||||
var feeds = state.Cursor.GetValue("feeds").AsDocumentObject;
|
||||
Assert.True(feeds.TryGetValue("alerts", out var published));
|
||||
Assert.Equal(DateTime.Parse("2025-10-11T05:30:00Z").ToUniversalTime(), published.ToUniversalTime());
|
||||
|
||||
var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsDocumentArray;
|
||||
Assert.Single(pendingDocuments);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var documentId = Guid.Parse(pendingDocuments[0]!.AsString);
|
||||
var document = await documentStore.FindAsync(documentId, CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
Assert.Equal(DocumentStatuses.PendingParse, document!.Status);
|
||||
var directMetadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
Assert.True(directMetadata.TryGetValue("acsc.fetch.mode", out var mode));
|
||||
Assert.Equal("direct", mode);
|
||||
}
|
||||
|
||||
Assert.Equal(DocumentStatuses.PendingParse, document!.Status);
|
||||
var directMetadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
Assert.True(directMetadata.TryGetValue("acsc.fetch.mode", out var mode));
|
||||
Assert.Equal("direct", mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_DirectFailureFallsBackToRelay()
|
||||
{
|
||||
@@ -90,20 +90,20 @@ public sealed class AcscConnectorFetchTests
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal("Relay", state!.Cursor.GetValue("preferredEndpoint").AsString);
|
||||
|
||||
var feeds = state.Cursor.GetValue("feeds").AsBsonDocument;
|
||||
Assert.True(feeds.TryGetValue("alerts", out var published));
|
||||
Assert.Equal(DateTime.Parse("2025-10-11T00:00:00Z").ToUniversalTime(), published.ToUniversalTime());
|
||||
|
||||
var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray;
|
||||
|
||||
var feeds = state.Cursor.GetValue("feeds").AsDocumentObject;
|
||||
Assert.True(feeds.TryGetValue("alerts", out var published));
|
||||
Assert.Equal(DateTime.Parse("2025-10-11T00:00:00Z").ToUniversalTime(), published.ToUniversalTime());
|
||||
|
||||
var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsDocumentArray;
|
||||
Assert.Single(pendingDocuments);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var documentId = Guid.Parse(pendingDocuments[0]!.AsString);
|
||||
var document = await documentStore.FindAsync(documentId, CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
Assert.Equal(DocumentStatuses.PendingParse, document!.Status);
|
||||
var metadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
Assert.Equal(DocumentStatuses.PendingParse, document!.Status);
|
||||
var metadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
Assert.True(metadata.TryGetValue("acsc.fetch.mode", out var mode));
|
||||
Assert.Equal("relay", mode);
|
||||
|
||||
@@ -112,10 +112,10 @@ public sealed class AcscConnectorFetchTests
|
||||
{
|
||||
Assert.Equal(HttpMethod.Get, request.Method);
|
||||
Assert.Equal(AlertsDirectUri, request.Uri);
|
||||
},
|
||||
request =>
|
||||
{
|
||||
Assert.Equal(HttpMethod.Get, request.Method);
|
||||
},
|
||||
request =>
|
||||
{
|
||||
Assert.Equal(HttpMethod.Get, request.Method);
|
||||
Assert.Equal(AlertsRelayUri, request.Uri);
|
||||
});
|
||||
}
|
||||
@@ -160,34 +160,34 @@ public sealed class AcscConnectorFetchTests
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
};
|
||||
|
||||
response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue($"\"{mode}-etag\"");
|
||||
response.Content.Headers.LastModified = second;
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static string CreateRssPayload(DateTimeOffset first, DateTimeOffset second)
|
||||
{
|
||||
return $$"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Alerts</title>
|
||||
<link>https://origin.example/feeds/alerts</link>
|
||||
<item>
|
||||
<title>First</title>
|
||||
<link>https://origin.example/alerts/first</link>
|
||||
<pubDate>{{first.ToString("r", CultureInfo.InvariantCulture)}}</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Second</title>
|
||||
<link>https://origin.example/alerts/second</link>
|
||||
<pubDate>{{second.ToString("r", CultureInfo.InvariantCulture)}}</pubDate>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue($"\"{mode}-etag\"");
|
||||
response.Content.Headers.LastModified = second;
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static string CreateRssPayload(DateTimeOffset first, DateTimeOffset second)
|
||||
{
|
||||
return $$"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Alerts</title>
|
||||
<link>https://origin.example/feeds/alerts</link>
|
||||
<item>
|
||||
<title>First</title>
|
||||
<link>https://origin.example/alerts/first</link>
|
||||
<pubDate>{{first.ToString("r", CultureInfo.InvariantCulture)}}</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Second</title>
|
||||
<link>https://origin.example/alerts/second</link>
|
||||
<pubDate>{{second.ToString("r", CultureInfo.InvariantCulture)}}</pubDate>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Acsc;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
@@ -15,9 +15,9 @@ using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class AcscConnectorParseTests
|
||||
{
|
||||
@@ -29,7 +29,7 @@ public sealed class AcscConnectorParseTests
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_PersistsDtoAndAdvancesCursor()
|
||||
{
|
||||
@@ -56,41 +56,41 @@ public sealed class AcscConnectorParseTests
|
||||
var dtoRecord = await dtoStore.FindByDocumentIdAsync(document.Id, CancellationToken.None);
|
||||
Assert.NotNull(dtoRecord);
|
||||
Assert.Equal("acsc.feed.v1", dtoRecord!.SchemaVersion);
|
||||
|
||||
var payload = dtoRecord.Payload;
|
||||
Assert.NotNull(payload);
|
||||
|
||||
var payload = dtoRecord.Payload;
|
||||
Assert.NotNull(payload);
|
||||
Assert.Equal("alerts", payload.GetValue("feedSlug").AsString);
|
||||
Assert.Single(payload.GetValue("entries").AsBsonArray);
|
||||
Assert.Single(payload.GetValue("entries").AsDocumentArray);
|
||||
|
||||
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.DoesNotContain(document.Id.ToString(), state!.Cursor.GetValue("pendingDocuments").AsBsonArray.Select(v => v.AsString));
|
||||
Assert.Contains(document.Id.ToString(), state.Cursor.GetValue("pendingMappings").AsBsonArray.Select(v => v.AsString));
|
||||
Assert.DoesNotContain(document.Id.ToString(), state!.Cursor.GetValue("pendingDocuments").AsDocumentArray.Select(v => v.AsString));
|
||||
Assert.Contains(document.Id.ToString(), state.Cursor.GetValue("pendingMappings").AsDocumentArray.Select(v => v.AsString));
|
||||
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoriesStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoriesStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Single(advisories);
|
||||
|
||||
var ordered = advisories
|
||||
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
WriteOrAssertSnapshot(
|
||||
SnapshotSerializer.ToSnapshot(ordered),
|
||||
"acsc-advisories.snapshot.json");
|
||||
|
||||
var mappedDocument = await documentStore.FindAsync(document.Id, CancellationToken.None);
|
||||
Assert.NotNull(mappedDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, mappedDocument!.Status);
|
||||
|
||||
state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.GetValue("pendingMappings").AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
|
||||
var ordered = advisories
|
||||
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
WriteOrAssertSnapshot(
|
||||
SnapshotSerializer.ToSnapshot(ordered),
|
||||
"acsc-advisories.snapshot.json");
|
||||
|
||||
var mappedDocument = await documentStore.FindAsync(document.Id, CancellationToken.None);
|
||||
Assert.NotNull(mappedDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, mappedDocument!.Status);
|
||||
|
||||
state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.GetValue("pendingMappings").AsDocumentArray.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAsync_MultiEntryFeedProducesExpectedSnapshot()
|
||||
{
|
||||
@@ -117,12 +117,12 @@ public sealed class AcscConnectorParseTests
|
||||
var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
var dtoRecord = await dtoStore.FindByDocumentIdAsync(document!.Id, CancellationToken.None);
|
||||
Assert.NotNull(dtoRecord);
|
||||
var payload = dtoRecord!.Payload;
|
||||
Assert.NotNull(payload);
|
||||
var entries = payload.GetValue("entries").AsBsonArray;
|
||||
Assert.Equal(2, entries.Count);
|
||||
var fields = entries[0].AsBsonDocument.GetValue("fields").AsBsonDocument;
|
||||
Assert.NotNull(dtoRecord);
|
||||
var payload = dtoRecord!.Payload;
|
||||
Assert.NotNull(payload);
|
||||
var entries = payload.GetValue("entries").AsDocumentArray;
|
||||
Assert.Equal(2, entries.Count);
|
||||
var fields = entries[0].AsDocumentObject.GetValue("fields").AsDocumentObject;
|
||||
Assert.Equal("Critical", fields.GetValue("severity").AsString);
|
||||
Assert.Equal("ExampleCo Router X, ExampleCo Router Y", fields.GetValue("systemsAffected").AsString);
|
||||
|
||||
@@ -131,20 +131,20 @@ public sealed class AcscConnectorParseTests
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
|
||||
var ordered = advisories
|
||||
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
WriteOrAssertSnapshot(
|
||||
SnapshotSerializer.ToSnapshot(ordered),
|
||||
"acsc-advisories-multi.snapshot.json");
|
||||
|
||||
var affected = ordered.First(advisory => advisory.AffectedPackages.Any());
|
||||
Assert.Contains("ExampleCo Router X", affected.AffectedPackages[0].Identifier);
|
||||
Assert.Equal("critical", ordered.First(a => a.Severity is not null).Severity, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
var ordered = advisories
|
||||
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
WriteOrAssertSnapshot(
|
||||
SnapshotSerializer.ToSnapshot(ordered),
|
||||
"acsc-advisories-multi.snapshot.json");
|
||||
|
||||
var affected = ordered.First(advisory => advisory.AffectedPackages.Any());
|
||||
Assert.Contains("ExampleCo Router X", affected.AffectedPackages[0].Identifier);
|
||||
Assert.Equal("critical", ordered.First(a => a.Severity is not null).Severity, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync(Action<AcscOptions>? configure = null)
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero);
|
||||
@@ -177,29 +177,29 @@ public sealed class AcscConnectorParseTests
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
|
||||
private static void SeedRssResponse(CannedHttpMessageHandler handler, Uri uri)
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>ACSC Alerts</title>
|
||||
<link>https://origin.example/feeds/alerts</link>
|
||||
<lastBuildDate>Sun, 12 Oct 2025 04:20:00 GMT</lastBuildDate>
|
||||
<item>
|
||||
<title>ACSC-2025-001 Example Advisory</title>
|
||||
<link>https://origin.example/advisories/example</link>
|
||||
<guid>https://origin.example/advisories/example</guid>
|
||||
<pubDate>Sun, 12 Oct 2025 03:00:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-001</p>
|
||||
<p><strong>Advisory type:</strong> Alert</p>
|
||||
<p>First paragraph describing issue.</p>
|
||||
<p>Second paragraph with <a href="https://vendor.example/patch">Vendor patch</a>.</p>
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
<channel>
|
||||
<title>ACSC Alerts</title>
|
||||
<link>https://origin.example/feeds/alerts</link>
|
||||
<lastBuildDate>Sun, 12 Oct 2025 04:20:00 GMT</lastBuildDate>
|
||||
<item>
|
||||
<title>ACSC-2025-001 Example Advisory</title>
|
||||
<link>https://origin.example/advisories/example</link>
|
||||
<guid>https://origin.example/advisories/example</guid>
|
||||
<pubDate>Sun, 12 Oct 2025 03:00:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-001</p>
|
||||
<p><strong>Advisory type:</strong> Alert</p>
|
||||
<p>First paragraph describing issue.</p>
|
||||
<p>Second paragraph with <a href="https://vendor.example/patch">Vendor patch</a>.</p>
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
|
||||
@@ -208,9 +208,9 @@ public sealed class AcscConnectorParseTests
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"parse-etag\"");
|
||||
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 12, 4, 20, 0, TimeSpan.Zero);
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"parse-etag\"");
|
||||
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 12, 4, 20, 0, TimeSpan.Zero);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
@@ -220,35 +220,35 @@ public sealed class AcscConnectorParseTests
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>ACSC Advisories</title>
|
||||
<link>https://origin.example/feeds/advisories</link>
|
||||
<lastBuildDate>Sun, 12 Oct 2025 05:00:00 GMT</lastBuildDate>
|
||||
<item>
|
||||
<title>Critical router vulnerability</title>
|
||||
<link>https://origin.example/advisories/router-critical</link>
|
||||
<guid>https://origin.example/advisories/router-critical</guid>
|
||||
<pubDate>Sun, 12 Oct 2025 04:45:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-010</p>
|
||||
<p><strong>Severity:</strong> Critical</p>
|
||||
<p><strong>Systems affected:</strong> ExampleCo Router X, ExampleCo Router Y</p>
|
||||
<p>Remote code execution on ExampleCo routers. See <a href="https://vendor.example/router/patch">vendor patch</a>.</p>
|
||||
<p>CVE references: CVE-2025-0001</p>
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title>Information bulletin</title>
|
||||
<link>https://origin.example/advisories/info-bulletin</link>
|
||||
<guid>https://origin.example/advisories/info-bulletin</guid>
|
||||
<pubDate>Sun, 12 Oct 2025 02:30:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-011</p>
|
||||
<p><strong>Advisory type:</strong> Bulletin</p>
|
||||
<p>General guidance bulletin.</p>
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
<channel>
|
||||
<title>ACSC Advisories</title>
|
||||
<link>https://origin.example/feeds/advisories</link>
|
||||
<lastBuildDate>Sun, 12 Oct 2025 05:00:00 GMT</lastBuildDate>
|
||||
<item>
|
||||
<title>Critical router vulnerability</title>
|
||||
<link>https://origin.example/advisories/router-critical</link>
|
||||
<guid>https://origin.example/advisories/router-critical</guid>
|
||||
<pubDate>Sun, 12 Oct 2025 04:45:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-010</p>
|
||||
<p><strong>Severity:</strong> Critical</p>
|
||||
<p><strong>Systems affected:</strong> ExampleCo Router X, ExampleCo Router Y</p>
|
||||
<p>Remote code execution on ExampleCo routers. See <a href="https://vendor.example/router/patch">vendor patch</a>.</p>
|
||||
<p>CVE references: CVE-2025-0001</p>
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title>Information bulletin</title>
|
||||
<link>https://origin.example/advisories/info-bulletin</link>
|
||||
<guid>https://origin.example/advisories/info-bulletin</guid>
|
||||
<pubDate>Sun, 12 Oct 2025 02:30:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-011</p>
|
||||
<p><strong>Advisory type:</strong> Bulletin</p>
|
||||
<p>General guidance bulletin.</p>
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
|
||||
@@ -257,82 +257,82 @@ public sealed class AcscConnectorParseTests
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"multi-etag\"");
|
||||
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 12, 5, 0, 0, TimeSpan.Zero);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static void WriteOrAssertSnapshot(string snapshot, string filename)
|
||||
{
|
||||
if (ShouldUpdateFixtures() || !FixtureExists(filename))
|
||||
{
|
||||
var writable = GetWritableFixturePath(filename);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(writable)!);
|
||||
File.WriteAllText(writable, Normalize(snapshot));
|
||||
return;
|
||||
}
|
||||
|
||||
var expected = Normalize(File.ReadAllText(GetExistingFixturePath(filename)));
|
||||
var actual = Normalize(snapshot);
|
||||
|
||||
if (!string.Equals(expected, actual, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = Path.Combine(Path.GetDirectoryName(GetWritableFixturePath(filename))!, Path.GetFileNameWithoutExtension(filename) + ".actual.json");
|
||||
File.WriteAllText(actualPath, actual);
|
||||
}
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
private static bool ShouldUpdateFixtures()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("UPDATE_ACSC_FIXTURES");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal)
|
||||
|| string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetExistingFixturePath(string filename)
|
||||
{
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
var primary = Path.Combine(baseDir, "Acsc", "Fixtures", filename);
|
||||
if (File.Exists(primary))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
var secondary = Path.Combine(baseDir, "Fixtures", filename);
|
||||
if (File.Exists(secondary))
|
||||
{
|
||||
return secondary;
|
||||
}
|
||||
|
||||
var projectRelative = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Acsc", "Fixtures", filename);
|
||||
if (File.Exists(projectRelative))
|
||||
{
|
||||
return Path.GetFullPath(projectRelative);
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"Fixture '{filename}' not found.", filename);
|
||||
}
|
||||
|
||||
private static string GetWritableFixturePath(string filename)
|
||||
=> Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Acsc", "Fixtures", filename);
|
||||
|
||||
private static string Normalize(string value)
|
||||
=> value.Replace("\r\n", "\n", StringComparison.Ordinal).Trim();
|
||||
|
||||
private static bool FixtureExists(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = GetExistingFixturePath(filename);
|
||||
return true;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"multi-etag\"");
|
||||
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 12, 5, 0, 0, TimeSpan.Zero);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static void WriteOrAssertSnapshot(string snapshot, string filename)
|
||||
{
|
||||
if (ShouldUpdateFixtures() || !FixtureExists(filename))
|
||||
{
|
||||
var writable = GetWritableFixturePath(filename);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(writable)!);
|
||||
File.WriteAllText(writable, Normalize(snapshot));
|
||||
return;
|
||||
}
|
||||
|
||||
var expected = Normalize(File.ReadAllText(GetExistingFixturePath(filename)));
|
||||
var actual = Normalize(snapshot);
|
||||
|
||||
if (!string.Equals(expected, actual, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = Path.Combine(Path.GetDirectoryName(GetWritableFixturePath(filename))!, Path.GetFileNameWithoutExtension(filename) + ".actual.json");
|
||||
File.WriteAllText(actualPath, actual);
|
||||
}
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
private static bool ShouldUpdateFixtures()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("UPDATE_ACSC_FIXTURES");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal)
|
||||
|| string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetExistingFixturePath(string filename)
|
||||
{
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
var primary = Path.Combine(baseDir, "Acsc", "Fixtures", filename);
|
||||
if (File.Exists(primary))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
var secondary = Path.Combine(baseDir, "Fixtures", filename);
|
||||
if (File.Exists(secondary))
|
||||
{
|
||||
return secondary;
|
||||
}
|
||||
|
||||
var projectRelative = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Acsc", "Fixtures", filename);
|
||||
if (File.Exists(projectRelative))
|
||||
{
|
||||
return Path.GetFullPath(projectRelative);
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"Fixture '{filename}' not found.", filename);
|
||||
}
|
||||
|
||||
private static string GetWritableFixturePath(string filename)
|
||||
=> Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Acsc", "Fixtures", filename);
|
||||
|
||||
private static string Normalize(string value)
|
||||
=> value.Replace("\r\n", "\n", StringComparison.Ordinal).Trim();
|
||||
|
||||
private static bool FixtureExists(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = GetExistingFixturePath(filename);
|
||||
return true;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
public sealed class AcscHttpClientConfigurationTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddAcscConnector_ConfiguresHttpClientOptions()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddAcscConnector(options =>
|
||||
{
|
||||
options.BaseEndpoint = new Uri("https://origin.example/");
|
||||
options.RelayEndpoint = new Uri("https://relay.example/");
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(42);
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new AcscFeedOptions
|
||||
{
|
||||
Slug = "alerts",
|
||||
RelativePath = "/feeds/alerts/rss",
|
||||
Enabled = true,
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var monitor = provider.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>();
|
||||
var options = monitor.Get(AcscOptions.HttpClientName);
|
||||
|
||||
Assert.Equal("StellaOps/Concelier (+https://stella-ops.org)", options.UserAgent);
|
||||
Assert.Equal(HttpVersion.Version20, options.RequestVersion);
|
||||
Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, options.VersionPolicy);
|
||||
Assert.Equal(TimeSpan.FromSeconds(42), options.Timeout);
|
||||
Assert.Contains("origin.example", options.AllowedHosts, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains("relay.example", options.AllowedHosts, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Equal("application/rss+xml, application/atom+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.7", options.DefaultRequestHeaders["Accept"]);
|
||||
}
|
||||
}
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
public sealed class AcscHttpClientConfigurationTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddAcscConnector_ConfiguresHttpClientOptions()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddAcscConnector(options =>
|
||||
{
|
||||
options.BaseEndpoint = new Uri("https://origin.example/");
|
||||
options.RelayEndpoint = new Uri("https://relay.example/");
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(42);
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new AcscFeedOptions
|
||||
{
|
||||
Slug = "alerts",
|
||||
RelativePath = "/feeds/alerts/rss",
|
||||
Enabled = true,
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var monitor = provider.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>();
|
||||
var options = monitor.Get(AcscOptions.HttpClientName);
|
||||
|
||||
Assert.Equal("StellaOps/Concelier (+https://stella-ops.org)", options.UserAgent);
|
||||
Assert.Equal(HttpVersion.Version20, options.RequestVersion);
|
||||
Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, options.VersionPolicy);
|
||||
Assert.Equal(TimeSpan.FromSeconds(42), options.Timeout);
|
||||
Assert.Contains("origin.example", options.AllowedHosts, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains("relay.example", options.AllowedHosts, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Equal("application/rss+xml, application/atom+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.7", options.DefaultRequestHeaders["Accept"]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user