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,25 @@
{
"idx": "5868",
"title": "태그프리 제품 부적절한 권한 검증 취약점",
"summary": "태그프리사의 X-Free Uploader에서 발생하는 부적절한 권한 검증 취약점",
"contentHtml": "<p>태그프리사의 X-Free Uploader에서 권한 검증이 미흡하여 임의 파일 삭제가 가능합니다.</p>",
"severity": "High",
"published": "2025-07-31T06:30:23Z",
"updated": "2025-08-01T02:15:00Z",
"cveIds": [
"CVE-2025-29866"
],
"references": [
{
"url": "https://www.tagfree.com/security",
"label": "제조사 공지"
}
],
"products": [
{
"vendor": "태그프리",
"name": "X-Free Uploader",
"versions": "XFU 1.0.1.0084 ~ 2.0.1.0034"
}
]
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>KNVD 보안취약점</title>
<link>https://knvd.krcert.or.kr/</link>
<description>테스트 피드</description>
<language>ko</language>
<item>
<title>태그프리 제품 부적절한 권한 검증 취약점</title>
<link>https://knvd.krcert.or.kr/detailDos.do?IDX=5868</link>
<category>취약점정보</category>
<pubDate>Thu, 31 Jul 2025 06:30:23 GMT</pubDate>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,213 @@
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;
}

View File

@@ -0,0 +1,25 @@
<?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.Connector.Kisa/StellaOps.Concelier.Connector.Kisa.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Connector.Common.Tests/StellaOps.Concelier.Connector.Common.Tests.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<None Update="Fixtures/*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Fixtures/*.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>