up
This commit is contained in:
		| @@ -0,0 +1,44 @@ | ||||
| { | ||||
|   "id": "7a760e58-bd5f-4f37-8b87-1b61f2deb001", | ||||
|   "vulnerabilityId": "ADV123456", | ||||
|   "cveNumbers": [ | ||||
|     "CVE-2025-0001" | ||||
|   ], | ||||
|   "title": "Windows Kernel Elevation of Privilege Vulnerability", | ||||
|   "description": "An elevation of privilege vulnerability exists in the Windows kernel.", | ||||
|   "releaseDate": "2025-10-10T10:00:00Z", | ||||
|   "lastModifiedDate": "2025-10-14T11:00:00Z", | ||||
|   "severity": "Critical", | ||||
|   "threats": [ | ||||
|     { | ||||
|       "type": "Impact", | ||||
|       "description": "Elevation of Privilege", | ||||
|       "severity": "Important" | ||||
|     } | ||||
|   ], | ||||
|   "remediations": [ | ||||
|     { | ||||
|       "id": "1", | ||||
|       "type": "Security Update", | ||||
|       "description": "Install KB5031234 to address this vulnerability.", | ||||
|       "url": "https://support.microsoft.com/help/5031234", | ||||
|       "kbNumber": "KB5031234" | ||||
|     } | ||||
|   ], | ||||
|   "affectedProducts": [ | ||||
|     { | ||||
|       "productId": "Windows11-23H2-x64", | ||||
|       "productName": "Windows 11 Version 23H2 for x64-based Systems", | ||||
|       "platform": "Windows", | ||||
|       "architecture": "x64", | ||||
|       "buildNumber": "22631.3520", | ||||
|       "cpe": "cpe:/o:microsoft:windows_11:23H2" | ||||
|     } | ||||
|   ], | ||||
|   "cvssV3": { | ||||
|     "baseScore": 8.1, | ||||
|     "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H" | ||||
|   }, | ||||
|   "releaseNoteUrl": "https://msrc.microsoft.com/update-guide/vulnerability/ADV123456", | ||||
|   "cvrfUrl": "https://download.microsoft.com/msrc/2025/ADV123456.cvrf.zip" | ||||
| } | ||||
| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|   "value": [ | ||||
|     { | ||||
|       "id": "7a760e58-bd5f-4f37-8b87-1b61f2deb001", | ||||
|       "vulnerabilityId": "ADV123456", | ||||
|       "cveNumbers": [ | ||||
|         "CVE-2025-0001" | ||||
|       ], | ||||
|       "title": "Windows Kernel Elevation of Privilege Vulnerability", | ||||
|       "description": "An elevation of privilege vulnerability exists in the Windows kernel.", | ||||
|       "releaseDate": "2025-10-10T10:00:00Z", | ||||
|       "lastModifiedDate": "2025-10-14T11:00:00Z", | ||||
|       "severity": "Critical", | ||||
|       "cvrfUrl": "https://download.microsoft.com/msrc/2025/ADV123456.cvrf.zip" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -0,0 +1,200 @@ | ||||
| using System; | ||||
| 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.Feedser.Source.Common.Fetch; | ||||
| using StellaOps.Feedser.Source.Common; | ||||
| using StellaOps.Feedser.Source.Common.Testing; | ||||
| using StellaOps.Feedser.Source.Vndr.Msrc.Configuration; | ||||
| using StellaOps.Feedser.Source.Vndr.Msrc.Internal; | ||||
| using StellaOps.Feedser.Storage.Mongo; | ||||
| using StellaOps.Feedser.Storage.Mongo.Advisories; | ||||
| using StellaOps.Feedser.Storage.Mongo.Documents; | ||||
| using StellaOps.Feedser.Storage.Mongo.Dtos; | ||||
| using StellaOps.Feedser.Testing; | ||||
| using Xunit; | ||||
| using StellaOps.Feedser.Source.Common.Http; | ||||
|  | ||||
| namespace StellaOps.Feedser.Source.Vndr.Msrc.Tests; | ||||
|  | ||||
| [Collection("mongo-fixture")] | ||||
| public sealed class MsrcConnectorTests : IAsyncLifetime | ||||
| { | ||||
|     private static readonly Uri TokenUri = new("https://login.microsoftonline.com/11111111-1111-1111-1111-111111111111/oauth2/v2.0/token"); | ||||
|     private static readonly Uri SummaryUri = new("https://api.msrc.microsoft.com/sug/v2.0/vulnerabilities"); | ||||
|     private static readonly Uri DetailUri = new("https://api.msrc.microsoft.com/sug/v2.0/vulnerability/ADV123456"); | ||||
|  | ||||
|     private readonly MongoIntegrationFixture _fixture; | ||||
|     private readonly CannedHttpMessageHandler _handler; | ||||
|  | ||||
|     public MsrcConnectorTests(MongoIntegrationFixture fixture) | ||||
|     { | ||||
|         _fixture = fixture; | ||||
|         _handler = new CannedHttpMessageHandler(); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task FetchParseMap_ProducesCanonicalAdvisory() | ||||
|     { | ||||
|         await using var provider = await BuildServiceProviderAsync(); | ||||
|         SeedResponses(); | ||||
|  | ||||
|         var connector = provider.GetRequiredService<MsrcConnector>(); | ||||
|         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("ADV123456"); | ||||
|         advisory.Severity.Should().Be("critical"); | ||||
|         advisory.Aliases.Should().Contain("CVE-2025-0001"); | ||||
|         advisory.Aliases.Should().Contain("KB5031234"); | ||||
|         advisory.References.Should().Contain(reference => reference.Url == "https://msrc.microsoft.com/update-guide/vulnerability/ADV123456"); | ||||
|         advisory.References.Should().Contain(reference => reference.Url == "https://download.microsoft.com/msrc/2025/ADV123456.cvrf.zip"); | ||||
|         advisory.AffectedPackages.Should().HaveCount(1); | ||||
|         advisory.AffectedPackages[0].NormalizedVersions.Should().Contain(rule => rule.Scheme == "msrc.build" && rule.Value == "22631.3520"); | ||||
|         advisory.CvssMetrics.Should().Contain(metric => metric.BaseScore == 8.1); | ||||
|  | ||||
|         var stateRepository = provider.GetRequiredService<ISourceStateRepository>(); | ||||
|         var state = await stateRepository.TryGetAsync(MsrcConnectorPlugin.SourceName, CancellationToken.None); | ||||
|         state.Should().NotBeNull(); | ||||
|         state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue(); | ||||
|         pendingDocs!.AsBsonArray.Should().BeEmpty(); | ||||
|  | ||||
|         var documentStore = provider.GetRequiredService<IDocumentStore>(); | ||||
|         var cvrfDocument = await documentStore.FindBySourceAndUriAsync(MsrcConnectorPlugin.SourceName, "https://download.microsoft.com/msrc/2025/ADV123456.cvrf.zip", CancellationToken.None); | ||||
|         cvrfDocument.Should().NotBeNull(); | ||||
|         cvrfDocument!.Status.Should().Be(DocumentStatuses.Mapped); | ||||
|     } | ||||
|  | ||||
|     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.AddSingleton(TimeProvider.System); | ||||
|  | ||||
|         services.AddMongoStorage(options => | ||||
|         { | ||||
|             options.ConnectionString = _fixture.Runner.ConnectionString; | ||||
|             options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; | ||||
|             options.CommandTimeout = TimeSpan.FromSeconds(5); | ||||
|         }); | ||||
|  | ||||
|         services.AddSourceCommon(); | ||||
|         services.AddMsrcConnector(options => | ||||
|         { | ||||
|             options.TenantId = "11111111-1111-1111-1111-111111111111"; | ||||
|             options.ClientId = "client-id"; | ||||
|             options.ClientSecret = "secret"; | ||||
|             options.InitialLastModified = new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero); | ||||
|             options.RequestDelay = TimeSpan.Zero; | ||||
|             options.MaxAdvisoriesPerFetch = 10; | ||||
|             options.CursorOverlap = TimeSpan.FromMinutes(1); | ||||
|             options.DownloadCvrf = true; | ||||
|         }); | ||||
|  | ||||
|         services.Configure<HttpClientFactoryOptions>(MsrcOptions.HttpClientName, builderOptions => | ||||
|         { | ||||
|             builderOptions.HttpMessageHandlerBuilderActions.Add(builder => | ||||
|             { | ||||
|                 builder.PrimaryHandler = _handler; | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         services.Configure<HttpClientFactoryOptions>(MsrcOptions.TokenClientName, 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() | ||||
|     { | ||||
|         var summaryJson = ReadFixture("msrc-summary.json"); | ||||
|         var detailJson = ReadFixture("msrc-detail.json"); | ||||
|         var tokenJson = """{"token_type":"Bearer","expires_in":3600,"access_token":"fake-token"}"""; | ||||
|         var cvrfBytes = Encoding.UTF8.GetBytes("PK\x03\x04FAKECVRF"); | ||||
|  | ||||
|         _handler.SetFallback(request => | ||||
|         { | ||||
|             if (request.RequestUri is null) | ||||
|             { | ||||
|                 return new HttpResponseMessage(HttpStatusCode.BadRequest); | ||||
|             } | ||||
|  | ||||
|             if (request.RequestUri.Host.Contains("login.microsoftonline.com", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                 { | ||||
|                     Content = new StringContent(tokenJson, Encoding.UTF8, "application/json"), | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             if (request.RequestUri.AbsolutePath.EndsWith("/vulnerabilities", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                 { | ||||
|                     Content = new StringContent(summaryJson, Encoding.UTF8, "application/json"), | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             if (request.RequestUri.AbsolutePath.Contains("/vulnerability/ADV123456", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                 { | ||||
|                     Content = new StringContent(detailJson, Encoding.UTF8, "application/json"), | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             if (request.RequestUri.Host.Contains("download.microsoft.com", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                 { | ||||
|                     Content = new ByteArrayContent(cvrfBytes) | ||||
|                     { | ||||
|                         Headers = | ||||
|                         { | ||||
|                             ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/zip"), | ||||
|                         }, | ||||
|                     }, | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return new HttpResponseMessage(HttpStatusCode.NotFound) | ||||
|             { | ||||
|                 Content = new StringContent($"No canned response for {request.RequestUri}", Encoding.UTF8), | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private static string ReadFixture(string fileName) | ||||
|         => System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName)); | ||||
|  | ||||
|     public Task InitializeAsync() => Task.CompletedTask; | ||||
|  | ||||
|     public Task DisposeAsync() => Task.CompletedTask; | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net10.0</TargetFramework> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|     <Nullable>enable</Nullable> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="../StellaOps.Feedser.Source.Vndr.Msrc/StellaOps.Feedser.Source.Vndr.Msrc.csproj" /> | ||||
|     <ProjectReference Include="../StellaOps.Feedser.Source.Common/StellaOps.Feedser.Source.Common.csproj" /> | ||||
|     <ProjectReference Include="../StellaOps.Feedser.Storage.Mongo/StellaOps.Feedser.Storage.Mongo.csproj" /> | ||||
|     <ProjectReference Include="../StellaOps.Feedser.Testing/StellaOps.Feedser.Testing.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="FluentAssertions" Version="6.12.0" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <None Update="Fixtures\*.json"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </None> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
		Reference in New Issue
	
	Block a user