Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,13 @@
<article>
<h1>ICSA-25-123-01: Example ICS Advisory</h1>
<p>The Cybersecurity and Infrastructure Security Agency (CISA) is aware of vulnerabilities affecting ControlSuite 4.2.</p>
<p><strong>Vendor:</strong> Example Corp</p>
<p><strong>Products:</strong> ControlSuite 4.2</p>
<p><a href="https://files.cisa.gov/docs/icsa-25-123-01.pdf">Download PDF advisory</a></p>
<p>For additional information see the <a href="https://example.com/security/icsa-25-123-01">vendor bulletin</a>.</p>
<h2>Mitigations</h2>
<ul>
<li>Apply ControlSuite firmware version 4.2.1 or later.</li>
<li>Restrict network access to the engineering workstation and monitor remote connections.</li>
</ul>
</article>

View File

@@ -0,0 +1,9 @@
<article>
<h1>ICSMA-25-045-01: Example Medical Advisory</h1>
<p>HealthTech InfusionManager 2.1 devices contain multiple vulnerabilities.</p>
<p><strong>Vendor:</strong> HealthTech</p>
<p><strong>Products:</strong> InfusionManager 2.1</p>
<p><a href="https://www.cisa.gov/sites/default/files/2025-10/ICSMA-25-045-01_Supplement.pdf">Supplemental guidance</a></p>
<h2>Mitigations</h2>
<p>Contact HealthTech support to obtain firmware 2.1.5 and enable multi-factor authentication for remote sessions.</p>
</article>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>CISA ICS Advisories</title>
<item>
<title>ICSA-25-123-01: Example ICS Advisory</title>
<link>https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01</link>
<pubDate>Mon, 13 Oct 2025 12:00:00 GMT</pubDate>
<description><![CDATA[
<p><strong>Vendor:</strong> Example Corp</p>
<p><strong>Products:</strong> ControlSuite 4.2</p>
<p><a href="https://example.com/security/icsa-25-123-01.pdf">Download vendor PDF</a></p>
<p>CVE-2024-12345 allows remote code execution.</p>
]]></description>
</item>
<item>
<title>ICSMA-25-045-01: Example Medical Advisory</title>
<link>https://www.cisa.gov/news-events/ics-medical-advisories/icsma-25-045-01</link>
<pubDate>Tue, 14 Oct 2025 09:30:00 GMT</pubDate>
<description><![CDATA[
<p><strong>Vendor:</strong> HealthTech</p>
<p><strong>Products:</strong> InfusionManager 2.1</p>
<p>Multiple vulnerabilities including CVE-2025-11111 and CVE-2025-22222.</p>
]]></description>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Ics.Cisa;
using StellaOps.Concelier.Connector.Ics.Cisa.Internal;
using Xunit;
namespace StellaOps.Concelier.Connector.Ics.Cisa.Tests.IcsCisa;
public class IcsCisaConnectorMappingTests
{
private static readonly DateTimeOffset RecordedAt = new(2025, 10, 14, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void BuildReferences_MergesFeedAndDetailAttachments()
{
var dto = new IcsCisaAdvisoryDto
{
AdvisoryId = "ICSA-25-123-01",
Title = "Sample Advisory",
Link = "https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01",
Summary = "Summary",
DescriptionHtml = "<p>Summary</p>",
Published = RecordedAt,
Updated = RecordedAt,
IsMedical = false,
References = new[]
{
"https://example.org/advisory",
"https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01"
},
Attachments = new List<IcsCisaAttachmentDto>
{
new() { Title = "PDF Attachment", Url = "https://files.cisa.gov/docs/icsa-25-123-01.pdf" },
}
};
var references = IcsCisaConnector.BuildReferences(dto, RecordedAt);
Assert.Equal(3, references.Count);
Assert.Contains(references, reference => reference.Kind == "attachment" && reference.Url == "https://files.cisa.gov/docs/icsa-25-123-01.pdf");
Assert.Contains(references, reference => reference.Url == "https://example.org/advisory");
Assert.Contains(references, reference => reference.Url == "https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01");
}
[Fact]
public void BuildMitigationReferences_ProducesReferences()
{
var dto = new IcsCisaAdvisoryDto
{
AdvisoryId = "ICSA-25-999-01",
Title = "Mitigation Test",
Link = "https://www.cisa.gov/news-events/ics-advisories/icsa-25-999-01",
Mitigations = new[] { "Apply firmware 9.9.1", "Limit network access" },
Published = RecordedAt,
Updated = RecordedAt,
IsMedical = false,
};
var references = IcsCisaConnector.BuildMitigationReferences(dto, RecordedAt);
Assert.Equal(2, references.Count);
var first = references.First();
Assert.Equal("mitigation", first.Kind);
Assert.Equal("icscisa-mitigation", first.SourceTag);
Assert.EndsWith("#mitigation-1", first.Url, StringComparison.Ordinal);
Assert.Contains("Apply firmware", first.Summary);
}
[Fact]
public void BuildAffectedPackages_EmitsProductRangesWithSemVer()
{
var dto = new IcsCisaAdvisoryDto
{
AdvisoryId = "ICSA-25-456-02",
Title = "Vendor Advisory",
Link = "https://www.cisa.gov/news-events/ics-advisories/icsa-25-456-02",
DescriptionHtml = "",
Summary = null,
Published = RecordedAt,
Vendors = new[] { "Example Corp" },
Products = new[] { "ControlSuite 4.2" }
};
var packages = IcsCisaConnector.BuildAffectedPackages(dto, RecordedAt);
var productPackage = Assert.Single(packages);
Assert.Equal(AffectedPackageTypes.IcsVendor, productPackage.Type);
Assert.Equal("ControlSuite", productPackage.Identifier);
var range = Assert.Single(productPackage.VersionRanges);
Assert.Equal("product", range.RangeKind);
Assert.Equal("4.2", range.RangeExpression);
Assert.NotNull(range.Primitives);
Assert.Equal("Example Corp", range.Primitives!.VendorExtensions!["ics.vendors"]);
Assert.Equal("ControlSuite", range.Primitives.VendorExtensions!["ics.product"]);
Assert.NotNull(range.Primitives.SemVer);
Assert.Equal("4.2.0", range.Primitives.SemVer!.ExactValue);
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using StellaOps.Concelier.Connector.Ics.Cisa.Internal;
using Xunit;
namespace StellaOps.Concelier.Connector.Ics.Cisa.Tests.IcsCisa;
public class IcsCisaFeedParserTests
{
[Fact]
public void Parse_ReturnsAdvisories()
{
var parser = new IcsCisaFeedParser();
using var stream = File.OpenRead(Path.Combine("IcsCisa", "Fixtures", "sample-feed.xml"));
var advisories = parser.Parse(stream, isMedicalTopic: false, topicUri: new Uri("https://content.govdelivery.com/accounts/USDHSCISA/topics.rss"));
Assert.Equal(2, advisories.Count);
var first = advisories.First();
Console.WriteLine("Description:" + first.DescriptionHtml);
Console.WriteLine("Attachments:" + string.Join(",", first.Attachments.Select(a => a.Url)));
Console.WriteLine("References:" + string.Join(",", first.References));
Assert.Equal("ICSA-25-123-01", first.AdvisoryId);
Assert.Contains("CVE-2024-12345", first.CveIds);
Assert.Contains("Example Corp", first.Vendors);
Assert.Contains("ControlSuite 4.2", first.Products);
Assert.Contains(first.Attachments, attachment => attachment.Url == "https://example.com/security/icsa-25-123-01.pdf");
Assert.Contains(first.References, reference => reference == "https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01");
var second = advisories.Last();
Assert.True(second.IsMedical);
Assert.Contains("CVE-2025-11111", second.CveIds);
Assert.Contains("HealthTech", second.Vendors);
}
}

View File

@@ -0,0 +1,156 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Ics.Cisa;
using StellaOps.Concelier.Connector.Ics.Cisa.Configuration;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Testing;
using Xunit;
namespace StellaOps.Concelier.Connector.Ics.Cisa.Tests;
[Collection("mongo-fixture")]
public sealed class IcsCisaConnectorTests : IAsyncLifetime
{
private readonly MongoIntegrationFixture _fixture;
private readonly CannedHttpMessageHandler _handler = new();
public IcsCisaConnectorTests(MongoIntegrationFixture fixture)
{
_fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
}
[Fact]
public async Task FetchParseMap_EndToEnd_ProducesCanonicalAdvisories()
{
await using var provider = await BuildServiceProviderAsync();
RegisterResponses();
var connector = provider.GetRequiredService<IcsCisaConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
_handler.AssertNoPendingResponses();
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.Equal(2, advisories.Count);
var icsa = Assert.Single(advisories, advisory => advisory.AdvisoryKey == "ICSA-25-123-01");
Console.WriteLine("ProductsRaw:" + string.Join("|", icsa.AffectedPackages.SelectMany(p => p.Provenance).Select(p => p.Value ?? "<null>")));
Assert.Contains("CVE-2024-12345", icsa.Aliases);
Assert.Contains(icsa.References, reference => reference.Url == "https://example.com/security/icsa-25-123-01");
Assert.Contains(icsa.References, reference => reference.Url == "https://files.cisa.gov/docs/icsa-25-123-01.pdf" && reference.Kind == "attachment");
var icsaMitigations = icsa.References.Where(reference => reference.Kind == "mitigation").ToList();
Assert.Equal(2, icsaMitigations.Count);
Assert.Contains("Apply ControlSuite firmware version 4.2.1 or later.", icsaMitigations[0].Summary, StringComparison.Ordinal);
Assert.EndsWith("#mitigation-1", icsaMitigations[0].Url, StringComparison.Ordinal);
Assert.Contains("Restrict network access", icsaMitigations[1].Summary, StringComparison.Ordinal);
var controlSuitePackage = Assert.Single(icsa.AffectedPackages, package => string.Equals(package.Identifier, "ControlSuite", StringComparison.OrdinalIgnoreCase));
var controlSuiteRange = Assert.Single(controlSuitePackage.VersionRanges);
Assert.Equal("product", controlSuiteRange.RangeKind);
Assert.Equal("4.2", controlSuiteRange.RangeExpression);
Assert.NotNull(controlSuiteRange.Primitives);
Assert.NotNull(controlSuiteRange.Primitives!.SemVer);
Assert.Equal("4.2.0", controlSuiteRange.Primitives.SemVer!.ExactValue);
Assert.True(controlSuiteRange.Primitives.VendorExtensions!.TryGetValue("ics.product", out var controlSuiteProduct) && controlSuiteProduct == "ControlSuite");
Assert.True(controlSuiteRange.Primitives.VendorExtensions!.TryGetValue("ics.version", out var controlSuiteVersion) && controlSuiteVersion == "4.2");
Assert.True(controlSuiteRange.Primitives.VendorExtensions!.TryGetValue("ics.vendors", out var controlSuiteVendors) && controlSuiteVendors == "Example Corp");
var icsma = Assert.Single(advisories, advisory => advisory.AdvisoryKey == "ICSMA-25-045-01");
Assert.Contains("CVE-2025-11111", icsma.Aliases);
var icsmaMitigation = Assert.Single(icsma.References.Where(reference => reference.Kind == "mitigation"));
Assert.Contains("Contact HealthTech support", icsmaMitigation.Summary, StringComparison.Ordinal);
Assert.Contains(icsma.References, reference => reference.Url == "https://www.cisa.gov/sites/default/files/2025-10/ICSMA-25-045-01_Supplement.pdf");
var infusionPackage = Assert.Single(icsma.AffectedPackages, package => string.Equals(package.Identifier, "InfusionManager", StringComparison.OrdinalIgnoreCase));
var infusionRange = Assert.Single(infusionPackage.VersionRanges);
Assert.Equal("2.1", infusionRange.RangeExpression);
}
private async Task<ServiceProvider> BuildServiceProviderAsync()
{
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
_handler.Clear();
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton(_handler);
services.AddMongoStorage(options =>
{
options.ConnectionString = _fixture.Runner.ConnectionString;
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
options.CommandTimeout = TimeSpan.FromSeconds(5);
});
services.AddSourceCommon();
services.AddIcsCisaConnector(options =>
{
options.GovDeliveryCode = "TESTCODE";
options.TopicsEndpoint = new Uri("https://feed.test/topics.rss", UriKind.Absolute);
options.TopicIds.Clear();
options.TopicIds.Add("USDHSCISA_TEST");
options.RequestDelay = TimeSpan.Zero;
options.DetailBaseUri = new Uri("https://www.cisa.gov/", UriKind.Absolute);
options.AdditionalHosts.Add("files.cisa.gov");
});
services.Configure<HttpClientFactoryOptions>(IcsCisaOptions.HttpClientName, builder =>
{
builder.HttpMessageHandlerBuilderActions.Add(handlerBuilder =>
{
handlerBuilder.PrimaryHandler = _handler;
});
});
var provider = services.BuildServiceProvider();
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
await bootstrapper.InitializeAsync(CancellationToken.None);
return provider;
}
private void RegisterResponses()
{
var feedUri = new Uri("https://feed.test/topics.rss?code=TESTCODE&format=xml&topic_id=USDHSCISA_TEST", UriKind.Absolute);
_handler.AddResponse(feedUri, () => CreateTextResponse("IcsCisa/Fixtures/sample-feed.xml", "application/rss+xml"));
var icsaDetail = new Uri("https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01", UriKind.Absolute);
_handler.AddResponse(icsaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsa-25-123-01.html", "text/html"));
var icsmaDetail = new Uri("https://www.cisa.gov/news-events/ics-medical-advisories/icsma-25-045-01", UriKind.Absolute);
_handler.AddResponse(icsmaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsma-25-045-01.html", "text/html"));
}
private static HttpResponseMessage CreateTextResponse(string relativePath, string contentType)
{
var fullPath = Path.Combine(AppContext.BaseDirectory, relativePath);
var content = File.ReadAllText(fullPath);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(content, Encoding.UTF8, contentType),
};
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_handler.Clear();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,17 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/StellaOps.Concelier.Connector.Ics.Cisa.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="IcsCisa/Fixtures/**" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>