Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
This commit is contained in:
@@ -70,11 +70,11 @@ public class IcsCisaConnectorMappingTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildAffectedPackages_EmitsProductRangesWithSemVer()
|
||||
{
|
||||
var dto = new IcsCisaAdvisoryDto
|
||||
{
|
||||
AdvisoryId = "ICSA-25-456-02",
|
||||
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 = "",
|
||||
@@ -89,13 +89,54 @@ public class IcsCisaConnectorMappingTests
|
||||
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);
|
||||
}
|
||||
}
|
||||
var range = Assert.Single(productPackage.VersionRanges);
|
||||
Assert.Equal("product", range.RangeKind);
|
||||
Assert.Equal("4.2.0", range.RangeExpression);
|
||||
Assert.NotNull(range.Primitives);
|
||||
Assert.Equal("Example Corp", range.Primitives!.VendorExtensions!["ics.vendors"]);
|
||||
Assert.Equal("ControlSuite", range.Primitives.VendorExtensions!["ics.product"]);
|
||||
Assert.True(range.Primitives.VendorExtensions!.ContainsKey("ics.range.expression"));
|
||||
Assert.NotNull(range.Primitives.SemVer);
|
||||
Assert.Equal("4.2.0", range.Primitives.SemVer!.ExactValue);
|
||||
Assert.Equal("ics-cisa:ICSA-25-456-02:controlsuite", range.Provenance.Value);
|
||||
var normalizedRule = Assert.Single(productPackage.NormalizedVersions);
|
||||
Assert.Equal("semver", normalizedRule.Scheme);
|
||||
Assert.Equal("exact", normalizedRule.Type);
|
||||
Assert.Equal("4.2.0", normalizedRule.Value);
|
||||
Assert.Equal("ics-cisa:ICSA-25-456-02:controlsuite", normalizedRule.Notes);
|
||||
var packageProvenance = Assert.Single(productPackage.Provenance);
|
||||
Assert.Contains(ProvenanceFieldMasks.AffectedPackages, packageProvenance.FieldMask);
|
||||
Assert.Contains(ProvenanceFieldMasks.VersionRanges, packageProvenance.FieldMask);
|
||||
Assert.Contains(ProvenanceFieldMasks.NormalizedVersions, packageProvenance.FieldMask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildAffectedPackages_NormalizesRangeExpressions()
|
||||
{
|
||||
var dto = new IcsCisaAdvisoryDto
|
||||
{
|
||||
AdvisoryId = "ICSA-25-789-03",
|
||||
Title = "Range Advisory",
|
||||
Link = "https://www.cisa.gov/news-events/ics-advisories/icsa-25-789-03",
|
||||
DescriptionHtml = "",
|
||||
Published = RecordedAt,
|
||||
Vendors = new[] { "Range Corp" },
|
||||
Products = new[] { "Control Suite Firmware 1.0 - 2.0" }
|
||||
};
|
||||
|
||||
var packages = IcsCisaConnector.BuildAffectedPackages(dto, RecordedAt);
|
||||
|
||||
var productPackage = Assert.Single(packages);
|
||||
Assert.Equal("Control Suite Firmware", productPackage.Identifier);
|
||||
var range = Assert.Single(productPackage.VersionRanges);
|
||||
Assert.Equal("1.0.0 - 2.0.0", range.RangeExpression);
|
||||
Assert.NotNull(range.Primitives);
|
||||
Assert.Equal("ics-cisa:ICSA-25-789-03:control-suite-firmware", range.Provenance.Value);
|
||||
var rule = Assert.Single(productPackage.NormalizedVersions);
|
||||
Assert.Equal("semver", rule.Scheme);
|
||||
Assert.Equal("range", rule.Type);
|
||||
Assert.Equal("1.0.0", rule.Min);
|
||||
Assert.Equal("2.0.0", rule.Max);
|
||||
Assert.Equal("ics-cisa:ICSA-25-789-03:control-suite-firmware", rule.Notes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@ public sealed class IcsCisaConnectorTests : IAsyncLifetime
|
||||
|
||||
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>")));
|
||||
var icsa = Assert.Single(advisories, advisory => advisory.AdvisoryKey == "ICSA-25-123-01");
|
||||
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");
|
||||
@@ -88,7 +87,7 @@ public sealed class IcsCisaConnectorTests : IAsyncLifetime
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>국내 취약점 정보</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="domestic_contents">
|
||||
<table class="basicView">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="bg_tht" colspan="2">
|
||||
CVE-2025-29866 | 태그프리 제품 부적절한 권한 검증 취약점
|
||||
<span class="date">2025.07.31</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cont" colspan="2">
|
||||
<p>
|
||||
<span>□ 개요</span><br />
|
||||
<span> o 태그프리社의 X-Free Uploader에서 발생하는 부적절한 권한 검증 취약점</span>
|
||||
</p>
|
||||
<table class="severity">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>취약점 종류</td>
|
||||
<td>영향</td>
|
||||
<td>심각도</td>
|
||||
<td>CVSS</td>
|
||||
<td>CVE ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>부적절한 권한 검증</td>
|
||||
<td>데이터 변조</td>
|
||||
<td>High</td>
|
||||
<td>8.8</td>
|
||||
<td>CVE-2025-29866</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<span>□ 영향받는 제품 및 해결 방안</span>
|
||||
</p>
|
||||
<table class="product">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>제품</td>
|
||||
<td>영향받는 버전</td>
|
||||
<td>해결 버전</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">TAGFREE X-Free Uploader</td>
|
||||
<td>{{PRIMARY_VERSION}}</td>
|
||||
<td>XFU 1.0.1.0085</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{SECONDARY_VERSION}}</td>
|
||||
<td>XFU 2.0.1.0035</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<span>□ 참고사이트</span>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://www.tagfree.com/bbs/board.php?bo_table=wb_xfu_update">
|
||||
https://www.tagfree.com/bbs/board.php?bo_table=wb_xfu_update
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,213 +1,497 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Kisa.Configuration;
|
||||
using StellaOps.Concelier.Connector.Kisa.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kisa.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri FeedUri = new("https://test.local/rss/securityInfo.do");
|
||||
private static readonly Uri DetailApiUri = new("https://test.local/rssDetailData.do?IDX=5868");
|
||||
private static readonly Uri DetailPageUri = new("https://test.local/detailDos.do?IDX=5868");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public KisaConnectorTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
|
||||
advisories.Should().HaveCount(1);
|
||||
|
||||
var advisory = advisories[0];
|
||||
advisory.AdvisoryKey.Should().Be("5868");
|
||||
advisory.Language.Should().Be("ko");
|
||||
advisory.Aliases.Should().Contain("CVE-2025-29866");
|
||||
advisory.AffectedPackages.Should().Contain(package => package.Identifier.Contains("태그프리"));
|
||||
advisory.References.Should().Contain(reference => reference.Url == DetailPageUri.ToString());
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(KisaConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
state!.Cursor.Should().NotBeNull();
|
||||
state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
|
||||
pendingDocs!.AsBsonArray.Should().BeEmpty();
|
||||
state.Cursor.TryGetValue("pendingMappings", out var pendingMappings).Should().BeTrue();
|
||||
pendingMappings!.AsBsonArray.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Telemetry_RecordsMetrics()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses();
|
||||
|
||||
using var metrics = new KisaMetricCollector();
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
Sum(metrics.Measurements, "kisa.feed.success").Should().Be(1);
|
||||
Sum(metrics.Measurements, "kisa.feed.items").Should().BeGreaterThan(0);
|
||||
Sum(metrics.Measurements, "kisa.detail.success").Should().Be(1);
|
||||
Sum(metrics.Measurements, "kisa.detail.failures").Should().Be(0);
|
||||
Sum(metrics.Measurements, "kisa.parse.success").Should().Be(1);
|
||||
Sum(metrics.Measurements, "kisa.parse.failures").Should().Be(0);
|
||||
Sum(metrics.Measurements, "kisa.map.success").Should().Be(1);
|
||||
Sum(metrics.Measurements, "kisa.map.failures").Should().Be(0);
|
||||
}
|
||||
|
||||
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.AddKisaConnector(options =>
|
||||
{
|
||||
options.FeedUri = FeedUri;
|
||||
options.DetailApiUri = new Uri("https://test.local/rssDetailData.do");
|
||||
options.DetailPageUri = new Uri("https://test.local/detailDos.do");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.MaxAdvisoriesPerFetch = 10;
|
||||
options.MaxKnownAdvisories = 32;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(KisaOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
{
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedResponses()
|
||||
{
|
||||
AddXmlResponse(FeedUri, ReadFixture("kisa-feed.xml"));
|
||||
AddJsonResponse(DetailApiUri, ReadFixture("kisa-detail.json"));
|
||||
}
|
||||
|
||||
private void AddXmlResponse(Uri uri, string xml)
|
||||
{
|
||||
_handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(xml, Encoding.UTF8, "application/rss+xml"),
|
||||
});
|
||||
}
|
||||
|
||||
private void AddJsonResponse(Uri uri, string json)
|
||||
{
|
||||
_handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
});
|
||||
}
|
||||
|
||||
private static string ReadFixture(string fileName)
|
||||
=> System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName));
|
||||
|
||||
private static long Sum(IEnumerable<KisaMetricCollector.MetricMeasurement> measurements, string name)
|
||||
=> measurements.Where(m => m.Name == name).Sum(m => m.Value);
|
||||
|
||||
private sealed class KisaMetricCollector : IDisposable
|
||||
{
|
||||
private readonly MeterListener _listener;
|
||||
private readonly ConcurrentBag<MetricMeasurement> _measurements = new();
|
||||
|
||||
public KisaMetricCollector()
|
||||
{
|
||||
_listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == KisaDiagnostics.MeterName)
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
var tagList = new List<KeyValuePair<string, object?>>(tags.Length);
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
tagList.Add(tag);
|
||||
}
|
||||
|
||||
_measurements.Add(new MetricMeasurement(instrument.Name, measurement, tagList));
|
||||
});
|
||||
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<MetricMeasurement> Measurements => _measurements;
|
||||
|
||||
public void Dispose() => _listener.Dispose();
|
||||
|
||||
internal sealed record MetricMeasurement(string Name, long Value, IReadOnlyList<KeyValuePair<string, object?>> Tags);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Kisa.Configuration;
|
||||
using StellaOps.Concelier.Connector.Kisa.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kisa.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri FeedUri = new("https://test.local/rss/securityInfo.do");
|
||||
private static readonly Uri DetailPageUri = new("https://test.local/detailDos.do?IDX=5868");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public KisaConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
|
||||
advisories.Should().HaveCount(1);
|
||||
|
||||
var advisory = advisories[0];
|
||||
advisory.AdvisoryKey.Should().Be("5868");
|
||||
advisory.Language.Should().Be("ko");
|
||||
advisory.Aliases.Should().Contain("CVE-2025-29866");
|
||||
advisory.AffectedPackages.Should().Contain(package => package.Identifier.Contains("태그프리"));
|
||||
advisory.References.Should().Contain(reference => reference.Url == DetailPageUri.ToString());
|
||||
|
||||
var package = advisory.AffectedPackages.Single();
|
||||
var normalized = GetSingleNormalizedVersion(package);
|
||||
normalized.Scheme.Should().Be(NormalizedVersionSchemes.SemVer);
|
||||
normalized.Type.Should().Be(NormalizedVersionRuleTypes.Range);
|
||||
normalized.Min.Should().Be("1.0.1-fw.84");
|
||||
normalized.MinInclusive.Should().BeTrue();
|
||||
normalized.Max.Should().Be("2.0.1-fw.34");
|
||||
normalized.MaxInclusive.Should().BeTrue();
|
||||
|
||||
package.VersionRanges.Should().ContainSingle();
|
||||
var range = package.VersionRanges.Single();
|
||||
range.RangeKind.Should().Be("product");
|
||||
range.RangeExpression.Should().Be("XFU 1.0.1.0084 ~ 2.0.1.0034");
|
||||
var semVer = GetSemVer(range.Primitives);
|
||||
semVer.Introduced.Should().Be("1.0.1-fw.84");
|
||||
semVer.IntroducedInclusive.Should().BeTrue();
|
||||
semVer.Fixed.Should().Be("2.0.1-fw.34");
|
||||
semVer.FixedInclusive.Should().BeTrue();
|
||||
semVer.ConstraintExpression.Should().Be(">= 1.0.1-fw.84 <= 2.0.1-fw.34");
|
||||
var vendorExtensions = GetVendorExtensions(range.Primitives);
|
||||
vendorExtensions.Should().ContainKey("kisa.range.raw").WhoseValue.Should().Be("XFU 1.0.1.0084 ~ 2.0.1.0034");
|
||||
vendorExtensions.Should().ContainKey("kisa.range.prefix").WhoseValue.Should().Be("XFU");
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(KisaConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
state!.Cursor.Should().NotBeNull();
|
||||
state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
|
||||
pendingDocs!.AsBsonArray.Should().BeEmpty();
|
||||
state.Cursor.TryGetValue("pendingMappings", out var pendingMappings).Should().BeTrue();
|
||||
pendingMappings!.AsBsonArray.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ExclusiveUpperBound_ProducesExclusiveNormalizedRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses("XFU 3.2 이상 4.0 미만");
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisory = (await advisoryStore.GetRecentAsync(1, CancellationToken.None)).Single();
|
||||
|
||||
var package = advisory.AffectedPackages.Single();
|
||||
var normalized = GetSingleNormalizedVersion(package);
|
||||
normalized.Min.Should().Be("3.2.0");
|
||||
normalized.MinInclusive.Should().BeTrue();
|
||||
normalized.Max.Should().Be("4.0.0");
|
||||
normalized.MaxInclusive.Should().BeFalse();
|
||||
|
||||
var range = package.VersionRanges.Single();
|
||||
var semVer = GetSemVer(range.Primitives);
|
||||
semVer.FixedInclusive.Should().BeFalse();
|
||||
semVer.ConstraintExpression.Should().Be(">= 3.2.0 < 4.0.0");
|
||||
|
||||
var vendorExtensions = GetVendorExtensions(range.Primitives);
|
||||
vendorExtensions
|
||||
.Should().ContainKey("kisa.range.normalized")
|
||||
.WhoseValue.Should().Be(">= 3.2.0 < 4.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ExclusiveLowerBound_ProducesExclusiveNormalizedRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses("XFU 1.2.0 초과 2.4.0 이하");
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisory = (await advisoryStore.GetRecentAsync(1, CancellationToken.None)).Single();
|
||||
|
||||
var package = advisory.AffectedPackages.Single();
|
||||
var normalized = GetSingleNormalizedVersion(package);
|
||||
normalized.Min.Should().Be("1.2.0");
|
||||
normalized.MinInclusive.Should().BeFalse();
|
||||
normalized.Max.Should().Be("2.4.0");
|
||||
normalized.MaxInclusive.Should().BeTrue();
|
||||
|
||||
var range = package.VersionRanges.Single();
|
||||
var semVer = GetSemVer(range.Primitives);
|
||||
semVer.IntroducedInclusive.Should().BeFalse();
|
||||
semVer.FixedInclusive.Should().BeTrue();
|
||||
semVer.ConstraintExpression.Should().Be("> 1.2.0 <= 2.4.0");
|
||||
|
||||
var vendorExtensions = GetVendorExtensions(range.Primitives);
|
||||
vendorExtensions
|
||||
.Should().ContainKey("kisa.range.normalized")
|
||||
.WhoseValue.Should().Be("> 1.2.0 <= 2.4.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_SingleBound_ProducesMinimumOnlyConstraint()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses("XFU 5.0 이상");
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisory = (await advisoryStore.GetRecentAsync(1, CancellationToken.None)).Single();
|
||||
|
||||
var package = advisory.AffectedPackages.Single();
|
||||
var normalized = GetSingleNormalizedVersion(package);
|
||||
normalized.Min.Should().Be("5.0.0");
|
||||
normalized.MinInclusive.Should().BeTrue();
|
||||
normalized.Type.Should().Be(NormalizedVersionRuleTypes.GreaterThanOrEqual);
|
||||
normalized.Max.Should().BeNull();
|
||||
normalized.MaxInclusive.Should().BeNull();
|
||||
|
||||
_output.WriteLine($"normalized: scheme={normalized.Scheme}, type={normalized.Type}, min={normalized.Min}, minInclusive={normalized.MinInclusive}, max={normalized.Max}, maxInclusive={normalized.MaxInclusive}, notes={normalized.Notes}");
|
||||
|
||||
var range = package.VersionRanges.Single();
|
||||
var semVer = GetSemVer(range.Primitives);
|
||||
semVer.Introduced.Should().Be("5.0.0");
|
||||
semVer.Fixed.Should().BeNull();
|
||||
semVer.LastAffected.Should().BeNull();
|
||||
semVer.ConstraintExpression.Should().Be(">= 5.0.0");
|
||||
|
||||
_output.WriteLine($"semver: introduced={semVer.Introduced}, introducedInclusive={semVer.IntroducedInclusive}, fixed={semVer.Fixed}, fixedInclusive={semVer.FixedInclusive}, lastAffected={semVer.LastAffected}, lastAffectedInclusive={semVer.LastAffectedInclusive}, constraint={semVer.ConstraintExpression}");
|
||||
|
||||
var vendorExtensions = GetVendorExtensions(range.Primitives);
|
||||
vendorExtensions
|
||||
.Should().ContainKey("kisa.range.normalized")
|
||||
.WhoseValue.Should().Be(">= 5.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_UpperBoundOnlyExclusive_ProducesLessThanRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses("XFU 3.5 미만");
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisory = (await advisoryStore.GetRecentAsync(1, CancellationToken.None)).Single();
|
||||
|
||||
var package = advisory.AffectedPackages.Single();
|
||||
var normalized = GetSingleNormalizedVersion(package);
|
||||
normalized.Type.Should().Be(NormalizedVersionRuleTypes.LessThan);
|
||||
normalized.Min.Should().BeNull();
|
||||
normalized.Max.Should().Be("3.5.0");
|
||||
normalized.MaxInclusive.Should().BeFalse();
|
||||
|
||||
var range = package.VersionRanges.Single();
|
||||
var semVer = GetSemVer(range.Primitives);
|
||||
semVer.Fixed.Should().Be("3.5.0");
|
||||
semVer.FixedInclusive.Should().BeFalse();
|
||||
semVer.ConstraintExpression.Should().Be("< 3.5.0");
|
||||
|
||||
var vendorExtensions = GetVendorExtensions(range.Primitives);
|
||||
vendorExtensions
|
||||
.Should().ContainKey("kisa.range.normalized")
|
||||
.WhoseValue.Should().Be("< 3.5.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_UpperBoundOnlyInclusive_ProducesLessThanOrEqualRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses("XFU 4.2 이하");
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisory = (await advisoryStore.GetRecentAsync(1, CancellationToken.None)).Single();
|
||||
|
||||
var package = advisory.AffectedPackages.Single();
|
||||
var normalized = GetSingleNormalizedVersion(package);
|
||||
normalized.Type.Should().Be(NormalizedVersionRuleTypes.LessThanOrEqual);
|
||||
normalized.Max.Should().Be("4.2.0");
|
||||
normalized.MaxInclusive.Should().BeTrue();
|
||||
|
||||
var range = package.VersionRanges.Single();
|
||||
var semVer = GetSemVer(range.Primitives);
|
||||
semVer.Fixed.Should().Be("4.2.0");
|
||||
semVer.FixedInclusive.Should().BeTrue();
|
||||
semVer.ConstraintExpression.Should().Be("<= 4.2.0");
|
||||
|
||||
var vendorExtensions = GetVendorExtensions(range.Primitives);
|
||||
vendorExtensions
|
||||
.Should().ContainKey("kisa.range.normalized")
|
||||
.WhoseValue.Should().Be("<= 4.2.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_LowerBoundOnlyExclusive_ProducesGreaterThanRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses("XFU 1.9 초과");
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisory = (await advisoryStore.GetRecentAsync(1, CancellationToken.None)).Single();
|
||||
|
||||
var package = advisory.AffectedPackages.Single();
|
||||
var normalized = GetSingleNormalizedVersion(package);
|
||||
normalized.Type.Should().Be(NormalizedVersionRuleTypes.GreaterThan);
|
||||
normalized.Min.Should().Be("1.9.0");
|
||||
normalized.MinInclusive.Should().BeFalse();
|
||||
normalized.Max.Should().BeNull();
|
||||
|
||||
var range = package.VersionRanges.Single();
|
||||
var semVer = GetSemVer(range.Primitives);
|
||||
semVer.Introduced.Should().Be("1.9.0");
|
||||
semVer.IntroducedInclusive.Should().BeFalse();
|
||||
semVer.ConstraintExpression.Should().Be("> 1.9.0");
|
||||
|
||||
var vendorExtensions = GetVendorExtensions(range.Primitives);
|
||||
vendorExtensions
|
||||
.Should().ContainKey("kisa.range.normalized")
|
||||
.WhoseValue.Should().Be("> 1.9.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_InvalidSegment_ProducesFallbackRange()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses("지원 버전: 최신 업데이트 적용");
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisory = (await advisoryStore.GetRecentAsync(1, CancellationToken.None)).Single();
|
||||
|
||||
var package = advisory.AffectedPackages.Single();
|
||||
package.NormalizedVersions.Should().BeEmpty();
|
||||
|
||||
var range = package.VersionRanges.Single();
|
||||
range.RangeKind.Should().Be("string");
|
||||
range.RangeExpression.Should().Be("지원 버전: 최신 업데이트 적용");
|
||||
var vendorExtensions = GetVendorExtensions(range.Primitives);
|
||||
vendorExtensions
|
||||
.Should().ContainKey("kisa.range.raw")
|
||||
.WhoseValue.Should().Be("지원 버전: 최신 업데이트 적용");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Telemetry_RecordsMetrics()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses();
|
||||
|
||||
using var metrics = new KisaMetricCollector();
|
||||
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
Sum(metrics.Measurements, "kisa.feed.success").Should().Be(1);
|
||||
Sum(metrics.Measurements, "kisa.feed.items").Should().BeGreaterThan(0);
|
||||
Sum(metrics.Measurements, "kisa.detail.success").Should().Be(1);
|
||||
Sum(metrics.Measurements, "kisa.detail.failures").Should().Be(0);
|
||||
Sum(metrics.Measurements, "kisa.parse.success").Should().Be(1);
|
||||
Sum(metrics.Measurements, "kisa.parse.failures").Should().Be(0);
|
||||
Sum(metrics.Measurements, "kisa.map.success").Should().Be(1);
|
||||
Sum(metrics.Measurements, "kisa.map.failures").Should().Be(0);
|
||||
}
|
||||
|
||||
private static NormalizedVersionRule GetSingleNormalizedVersion(AffectedPackage package)
|
||||
{
|
||||
var normalizedVersions = package.NormalizedVersions;
|
||||
if (normalizedVersions.IsDefaultOrEmpty)
|
||||
{
|
||||
throw new InvalidOperationException("Expected normalized version rule.");
|
||||
}
|
||||
|
||||
return normalizedVersions.Single();
|
||||
}
|
||||
|
||||
private static SemVerPrimitive GetSemVer(RangePrimitives? primitives)
|
||||
=> primitives?.SemVer ?? throw new InvalidOperationException("Expected semver primitive.");
|
||||
|
||||
private static IReadOnlyDictionary<string, string> GetVendorExtensions(RangePrimitives? primitives)
|
||||
=> primitives?.VendorExtensions ?? throw new InvalidOperationException("Expected vendor extensions.");
|
||||
|
||||
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.AddKisaConnector(options =>
|
||||
{
|
||||
options.FeedUri = FeedUri;
|
||||
options.DetailApiUri = new Uri("https://test.local/rssDetailData.do");
|
||||
options.DetailPageUri = new Uri("https://test.local/detailDos.do");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.MaxAdvisoriesPerFetch = 10;
|
||||
options.MaxKnownAdvisories = 32;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(KisaOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
{
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedResponses(string? versionOverride = null)
|
||||
{
|
||||
AddXmlResponse(FeedUri, ReadFixture("kisa-feed.xml"));
|
||||
var detailPayload = BuildDetailHtml(versionOverride);
|
||||
AddHtmlResponse(DetailPageUri, detailPayload);
|
||||
}
|
||||
|
||||
private void AddXmlResponse(Uri uri, string xml)
|
||||
{
|
||||
_handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(xml, Encoding.UTF8, "application/rss+xml"),
|
||||
});
|
||||
}
|
||||
|
||||
private void AddHtmlResponse(Uri uri, string html)
|
||||
{
|
||||
_handler.AddResponse(uri, () => new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(html, Encoding.UTF8, "text/html"),
|
||||
});
|
||||
}
|
||||
|
||||
private static string ReadFixture(string fileName)
|
||||
=> System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName));
|
||||
|
||||
private static string BuildDetailHtml(string? versions)
|
||||
{
|
||||
var template = ReadFixture("kisa-detail.html");
|
||||
var primary = versions ?? "XFU 1.0.1.0084";
|
||||
var secondary = versions is null ? "XFU 2.0.1.0034" : string.Empty;
|
||||
|
||||
return template
|
||||
.Replace("{{PRIMARY_VERSION}}", primary, StringComparison.Ordinal)
|
||||
.Replace("{{SECONDARY_VERSION}}", secondary, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static long Sum(IEnumerable<KisaMetricCollector.MetricMeasurement> measurements, string name)
|
||||
=> measurements.Where(m => m.Name == name).Sum(m => m.Value);
|
||||
|
||||
private sealed class KisaMetricCollector : IDisposable
|
||||
{
|
||||
private readonly MeterListener _listener;
|
||||
private readonly ConcurrentBag<MetricMeasurement> _measurements = new();
|
||||
|
||||
public KisaMetricCollector()
|
||||
{
|
||||
_listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == KisaDiagnostics.MeterName)
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
var tagList = new List<KeyValuePair<string, object?>>(tags.Length);
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
tagList.Add(tag);
|
||||
}
|
||||
|
||||
_measurements.Add(new MetricMeasurement(instrument.Name, measurement, tagList));
|
||||
});
|
||||
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<MetricMeasurement> Measurements => _measurements;
|
||||
|
||||
public void Dispose() => _listener.Dispose();
|
||||
|
||||
internal sealed record MetricMeasurement(string Name, long Value, IReadOnlyList<KeyValuePair<string, object?>> Tags);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Concelier.Connector.Common.Html;
|
||||
using StellaOps.Concelier.Connector.Kisa.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kisa.Tests;
|
||||
|
||||
public sealed class KisaDetailParserTests
|
||||
{
|
||||
private static readonly Uri DetailApiUri = new("https://test.local/rssDetailData.do?IDX=5868");
|
||||
private static readonly Uri DetailPageUri = new("https://test.local/detailDos.do?IDX=5868");
|
||||
|
||||
[Fact]
|
||||
public void ParseHtmlPayload_ProducesExpectedModels()
|
||||
{
|
||||
var parser = new KisaDetailParser(new HtmlContentSanitizer());
|
||||
var payload = ReadFixtureBytes("kisa-detail.html", "XFU 1.0.1.0084", "XFU 2.0.1.0034");
|
||||
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["kisa.idx"] = "5868",
|
||||
["kisa.title"] = "태그프리 제품 부적절한 권한 검증 취약점",
|
||||
["kisa.published"] = "2025-07-31T06:30:23Z",
|
||||
};
|
||||
|
||||
var parsed = parser.Parse(DetailApiUri, DetailPageUri, payload, metadata);
|
||||
|
||||
parsed.AdvisoryId.Should().Be("5868");
|
||||
parsed.Title.Should().Contain("태그프리");
|
||||
parsed.Summary.Should().NotBeNullOrWhiteSpace();
|
||||
parsed.ContentHtml.Should().Contain("TAGFREE");
|
||||
parsed.Severity.Should().Be("High");
|
||||
parsed.CveIds.Should().Contain("CVE-2025-29866");
|
||||
|
||||
parsed.Products.Should().ContainSingle();
|
||||
var product = parsed.Products.Single();
|
||||
product.Vendor.Should().Be("태그프리");
|
||||
product.Name.Should().Be("X-Free Uploader");
|
||||
product.Versions.Should().Be("XFU 1.0.1.0084 ~ 2.0.1.0034");
|
||||
}
|
||||
|
||||
private static byte[] ReadFixtureBytes(string fileName, string primaryVersion, string secondaryVersion)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName);
|
||||
var template = File.ReadAllText(path);
|
||||
var html = template
|
||||
.Replace("{{PRIMARY_VERSION}}", primaryVersion, StringComparison.Ordinal)
|
||||
.Replace("{{SECONDARY_VERSION}}", secondaryVersion, StringComparison.Ordinal);
|
||||
return Encoding.UTF8.GetBytes(html);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Fixtures/*.json">
|
||||
@@ -21,5 +22,8 @@
|
||||
<None Update="Fixtures/*.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Fixtures/*.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user