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,259 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Common.Packages;
using StellaOps.Concelier.Connector.Vndr.Apple;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.PsirtFlags;
using StellaOps.Concelier.Testing;
using Xunit;
namespace StellaOps.Concelier.Connector.Vndr.Apple.Tests;
[Collection("mongo-fixture")]
public sealed class AppleConnectorTests : IAsyncLifetime
{
private static readonly Uri IndexUri = new("https://support.example.com/index.json");
private static readonly Uri DetailBaseUri = new("https://support.example.com/en-us/");
private readonly MongoIntegrationFixture _fixture;
private readonly FakeTimeProvider _timeProvider;
public AppleConnectorTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero));
}
[Fact]
public async Task FetchParseMap_EndToEnd_ProducesCanonicalAdvisories()
{
var handler = new CannedHttpMessageHandler();
SeedIndex(handler);
SeedDetail(handler);
await using var provider = await BuildServiceProviderAsync(handler);
var connector = provider.GetRequiredService<AppleConnector>();
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(10, CancellationToken.None);
Assert.Equal(5, advisories.Count);
var advisoriesByKey = advisories.ToDictionary(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal);
var iosBaseline = advisoriesByKey["125326"];
Assert.Contains("CVE-2025-43400", iosBaseline.Aliases, StringComparer.OrdinalIgnoreCase);
Assert.Equal(3, iosBaseline.AffectedPackages.Count());
AssertPackageDetails(iosBaseline, "iPhone 16 Pro", "26.0.1", "24A341", "iOS");
AssertPackageDetails(iosBaseline, "iPhone 16 Pro", "26.0.1 (a)", "24A341a", "iOS");
AssertPackageDetails(iosBaseline, "iPad Pro (M4)", "26", "24B120", "iPadOS");
var macBaseline = advisoriesByKey["125328"];
Assert.Contains("CVE-2025-43400", macBaseline.Aliases, StringComparer.OrdinalIgnoreCase);
Assert.Equal(2, macBaseline.AffectedPackages.Count());
AssertPackageDetails(macBaseline, "MacBook Pro (M4)", "26.0.1", "26A123", "macOS");
AssertPackageDetails(macBaseline, "Mac Studio", "26", "26A120b", "macOS");
var venturaRsr = advisoriesByKey["106355"];
Assert.Contains("CVE-2023-37450", venturaRsr.Aliases, StringComparer.OrdinalIgnoreCase);
Assert.Equal(2, venturaRsr.AffectedPackages.Count());
AssertPackageDetails(venturaRsr, "macOS Ventura", string.Empty, "22F400", "macOS Ventura");
AssertPackageDetails(venturaRsr, "macOS Ventura (Intel)", string.Empty, "22F400a", "macOS Ventura");
var visionOs = advisoriesByKey["HT214108"];
Assert.Contains("CVE-2024-27800", visionOs.Aliases, StringComparer.OrdinalIgnoreCase);
Assert.False(visionOs.AffectedPackages.Any());
var rsrAdvisory = advisoriesByKey["HT215500"];
Assert.Contains("CVE-2025-2468", rsrAdvisory.Aliases, StringComparer.OrdinalIgnoreCase);
var rsrPackage = AssertPackageDetails(rsrAdvisory, "iPhone 15 Pro", "18.0.1 (c)", "22A123c", "iOS");
Assert.True(rsrPackage.NormalizedVersions.IsDefaultOrEmpty || rsrPackage.NormalizedVersions.Length == 0);
var flagStore = provider.GetRequiredService<IPsirtFlagStore>();
var rsrFlag = await flagStore.FindAsync("HT215500", CancellationToken.None);
Assert.NotNull(rsrFlag);
}
private static AffectedPackage AssertPackageDetails(
Advisory advisory,
string identifier,
string expectedRangeExpression,
string? expectedBuild,
string expectedPlatform)
{
var candidates = advisory.AffectedPackages
.Where(package => string.Equals(package.Identifier, identifier, StringComparison.Ordinal))
.ToList();
Assert.NotEmpty(candidates);
var package = Assert.Single(candidates, candidate =>
{
var rangeCandidate = candidate.VersionRanges.SingleOrDefault();
return rangeCandidate is not null
&& string.Equals(rangeCandidate.RangeExpression ?? string.Empty, expectedRangeExpression, StringComparison.Ordinal);
});
Assert.Equal(expectedPlatform, package.Platform);
var range = Assert.Single(package.VersionRanges);
Assert.Equal(expectedRangeExpression, range.RangeExpression ?? string.Empty);
Assert.Equal("vendor", range.RangeKind);
Assert.NotNull(range.Primitives);
Assert.NotNull(range.Primitives!.VendorExtensions);
var vendorExtensions = range.Primitives.VendorExtensions;
if (!string.IsNullOrWhiteSpace(expectedRangeExpression))
{
if (!vendorExtensions.TryGetValue("apple.version.raw", out var rawVersion))
{
throw new Xunit.Sdk.XunitException($"Missing apple.version.raw for {identifier}; available keys: {string.Join(", ", vendorExtensions.Keys)}");
}
Assert.Equal(expectedRangeExpression, rawVersion);
}
else
{
Assert.False(vendorExtensions.ContainsKey("apple.version.raw"));
}
if (!string.IsNullOrWhiteSpace(expectedPlatform))
{
if (vendorExtensions.TryGetValue("apple.platform", out var platformExtension))
{
Assert.Equal(expectedPlatform, platformExtension);
}
else
{
throw new Xunit.Sdk.XunitException($"Missing apple.platform extension for {identifier}; available keys: {string.Join(", ", vendorExtensions.Keys)}");
}
}
if (!string.IsNullOrWhiteSpace(expectedBuild))
{
Assert.True(vendorExtensions.TryGetValue("apple.build", out var buildExtension));
Assert.Equal(expectedBuild, buildExtension);
}
else
{
Assert.False(vendorExtensions.ContainsKey("apple.build"));
}
if (PackageCoordinateHelper.TryParseSemVer(expectedRangeExpression, out _, out var normalized))
{
var normalizedRule = Assert.Single(package.NormalizedVersions);
Assert.Equal(NormalizedVersionSchemes.SemVer, normalizedRule.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThanOrEqual, normalizedRule.Type);
Assert.Equal(normalized, normalizedRule.Max);
Assert.Equal($"apple:{expectedPlatform}:{identifier}", normalizedRule.Notes);
}
else
{
Assert.True(package.NormalizedVersions.IsDefaultOrEmpty || package.NormalizedVersions.Length == 0);
}
return package;
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler)
{
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton<TimeProvider>(_timeProvider);
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.AddAppleConnector(opts =>
{
opts.SoftwareLookupUri = IndexUri;
opts.AdvisoryBaseUri = DetailBaseUri;
opts.LocaleSegment = "en-us";
opts.InitialBackfill = TimeSpan.FromDays(120);
opts.ModifiedTolerance = TimeSpan.FromHours(2);
opts.MaxAdvisoriesPerFetch = 10;
});
services.Configure<HttpClientFactoryOptions>(AppleOptions.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 static void SeedIndex(CannedHttpMessageHandler handler)
{
handler.AddJsonResponse(IndexUri, ReadFixture("index.json"));
}
private static void SeedDetail(CannedHttpMessageHandler handler)
{
AddHtmlResponse(handler, new Uri(DetailBaseUri, "125326"), "125326.html");
AddHtmlResponse(handler, new Uri(DetailBaseUri, "125328"), "125328.html");
AddHtmlResponse(handler, new Uri(DetailBaseUri, "106355"), "106355.html");
AddHtmlResponse(handler, new Uri(DetailBaseUri, "HT214108"), "ht214108.html");
AddHtmlResponse(handler, new Uri(DetailBaseUri, "HT215500"), "ht215500.html");
}
private static void AddHtmlResponse(CannedHttpMessageHandler handler, Uri uri, string fixture)
{
handler.AddResponse(uri, () =>
{
var content = ReadFixture(fixture);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(content, Encoding.UTF8, "text/html"),
};
});
}
private static string ReadFixture(string name)
{
var path = Path.Combine(
AppContext.BaseDirectory,
"Source",
"Vndr",
"Apple",
"Fixtures",
name);
return File.ReadAllText(path);
}
}

View File

@@ -0,0 +1,342 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Concelier.Connector.Vndr.Apple.Internal;
namespace StellaOps.Concelier.Connector.Vndr.Apple.Tests.Apple;
internal static class AppleFixtureManager
{
private const string UpdateEnvVar = "UPDATE_APPLE_FIXTURES";
private const string UpdateSentinelFileName = ".update-apple-fixtures";
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
private static readonly Uri ExampleDetailBaseUri = new("https://support.example.com/en-us/");
private static readonly AppleFixtureDefinition[] Definitions =
{
new(
"125326",
new Uri("https://support.apple.com/en-us/125326"),
Products: new[]
{
new AppleFixtureProduct("iOS", "iPhone 16 Pro", "26.0.1", "24A341"),
new AppleFixtureProduct("iOS", "iPhone 16 Pro", "26.0.1 (a)", "24A341a"),
new AppleFixtureProduct("iPadOS", "iPad Pro (M4)", "26", "24B120"),
}),
new(
"125328",
new Uri("https://support.apple.com/en-us/125328"),
Products: new[]
{
new AppleFixtureProduct("macOS", "MacBook Pro (M4)", "26.0.1", "26A123"),
new AppleFixtureProduct("macOS", "Mac Studio", "26", "26A120b"),
}),
new(
"106355",
new Uri("https://support.apple.com/en-us/106355"),
ForceRapidSecurityResponse: true,
Products: new[]
{
new AppleFixtureProduct("macOS Ventura", "macOS Ventura", string.Empty, "22F400"),
new AppleFixtureProduct("macOS Ventura", "macOS Ventura (Intel)", string.Empty, "22F400a"),
}),
new(
"HT214108",
new Uri("https://support.apple.com/en-us/HT214108")),
new(
"HT215500",
new Uri("https://support.apple.com/en-us/HT215500"),
ForceRapidSecurityResponse: true),
};
private static readonly Lazy<Task> UpdateTask = new(
() => UpdateFixturesAsync(CancellationToken.None),
LazyThreadSafetyMode.ExecutionAndPublication);
public static IReadOnlyList<AppleFixtureDefinition> Fixtures => Definitions;
public static Task EnsureUpdatedAsync(CancellationToken cancellationToken = default)
{
if (!ShouldUpdateFixtures())
{
return Task.CompletedTask;
}
Console.WriteLine("[AppleFixtures] UPDATE flag detected; refreshing fixtures");
return UpdateTask.Value;
}
public static string ReadFixture(string name)
{
var path = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Apple", "Fixtures", name);
return File.ReadAllText(path);
}
public static AppleDetailDto ReadExpectedDto(string articleId)
{
var json = ReadFixture($"{articleId}.expected.json");
return JsonSerializer.Deserialize<AppleDetailDto>(json, SerializerOptions)
?? throw new InvalidOperationException($"Unable to deserialize expected DTO for {articleId}.");
}
private static bool ShouldUpdateFixtures()
{
var value = Environment.GetEnvironmentVariable(UpdateEnvVar)?.Trim();
if (string.IsNullOrEmpty(value))
{
var sentinelPath = Path.Combine(ResolveFixtureRoot(), UpdateSentinelFileName);
return File.Exists(sentinelPath);
}
if (string.Equals(value, "0", StringComparison.Ordinal)
|| string.Equals(value, "false", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
private static async Task UpdateFixturesAsync(CancellationToken cancellationToken)
{
var handler = new HttpClientHandler
{
AutomaticDecompression = System.Net.DecompressionMethods.All,
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
};
using var httpClient = new HttpClient(handler);
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOpsFixtureUpdater", "1.0"));
httpClient.Timeout = TimeSpan.FromSeconds(30);
var indexEntries = new List<object>(Definitions.Length);
try
{
foreach (var definition in Definitions)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var html = await httpClient.GetStringAsync(definition.DetailUri, cancellationToken).ConfigureAwait(false);
var entryProducts = definition.Products?
.Select(product => new AppleIndexProduct(
product.Platform,
product.Name,
product.Version,
product.Build))
.ToArray() ?? Array.Empty<AppleIndexProduct>();
var entry = new AppleIndexEntry(
UpdateId: definition.ArticleId,
ArticleId: definition.ArticleId,
Title: definition.ArticleId,
PostingDate: DateTimeOffset.UtcNow,
DetailUri: definition.DetailUri,
Products: entryProducts,
IsRapidSecurityResponse: definition.ForceRapidSecurityResponse ?? false);
var dto = AppleDetailParser.Parse(html, entry);
var sanitizedHtml = BuildSanitizedHtml(dto);
WriteFixture(definition.HtmlFixtureName, sanitizedHtml);
WriteFixture(definition.ExpectedFixtureName, JsonSerializer.Serialize(dto, SerializerOptions));
var exampleDetailUri = new Uri(ExampleDetailBaseUri, definition.ArticleId);
indexEntries.Add(new
{
id = definition.ArticleId,
articleId = definition.ArticleId,
title = dto.Title,
postingDate = dto.Published.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
detailUrl = exampleDetailUri.ToString(),
rapidSecurityResponse = definition.ForceRapidSecurityResponse
?? dto.Title.Contains("Rapid Security Response", StringComparison.OrdinalIgnoreCase),
products = dto.Affected
.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
.Select(p => new
{
platform = p.Platform,
name = p.Name,
version = p.Version,
build = p.Build,
})
.ToArray(),
});
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or IOException)
{
Console.WriteLine($"[AppleFixtures] Skipped {definition.ArticleId}: {ex.Message}");
}
}
}
finally
{
var sentinelPath = Path.Combine(ResolveFixtureRoot(), UpdateSentinelFileName);
if (File.Exists(sentinelPath))
{
try
{
File.Delete(sentinelPath);
}
catch (IOException)
{
// best effort
}
}
}
var indexDocument = new { updates = indexEntries };
WriteFixture("index.json", JsonSerializer.Serialize(indexDocument, SerializerOptions));
}
private static string BuildSanitizedHtml(AppleDetailDto dto)
{
var builder = new StringBuilder();
builder.AppendLine("<!DOCTYPE html>");
builder.AppendLine("<html lang=\"en\">");
builder.AppendLine("<body>");
builder.AppendLine("<article>");
builder.AppendLine($" <h1 data-testid=\"update-title\">{Escape(dto.Title)}</h1>");
if (!string.IsNullOrWhiteSpace(dto.Summary))
{
builder.AppendLine($" <p data-testid=\"update-summary\">{Escape(dto.Summary)}</p>");
}
var publishedDisplay = dto.Published.ToString("MMMM d, yyyy", CultureInfo.InvariantCulture);
builder.AppendLine($" <time data-testid=\"published\" datetime=\"{dto.Published:O}\">{publishedDisplay}</time>");
if (dto.Updated.HasValue)
{
var updatedDisplay = dto.Updated.Value.ToString("MMMM d, yyyy", CultureInfo.InvariantCulture);
builder.AppendLine($" <time data-testid=\"updated\" datetime=\"{dto.Updated:O}\">{updatedDisplay}</time>");
}
if (dto.CveIds.Count > 0)
{
builder.AppendLine(" <section data-component=\"security-update\">");
builder.AppendLine(" <h2>Security Issues</h2>");
builder.AppendLine(" <ul data-testid=\"cve-list\">");
foreach (var cve in dto.CveIds.OrderBy(id => id, StringComparer.OrdinalIgnoreCase))
{
builder.AppendLine($" <li>{Escape(cve)}</li>");
}
builder.AppendLine(" </ul>");
builder.AppendLine(" </section>");
}
if (dto.Affected.Count > 0)
{
builder.AppendLine(" <table>");
builder.AppendLine(" <tbody>");
foreach (var product in dto.Affected
.OrderBy(p => p.Platform ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ThenBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(p => p.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ThenBy(p => p.Build ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
var platform = product.Platform is null ? string.Empty : Escape(product.Platform);
var name = Escape(product.Name);
var version = product.Version is null ? string.Empty : Escape(product.Version);
var build = product.Build is null ? string.Empty : Escape(product.Build);
builder.Append(" <tr data-testid=\"product-row\"");
builder.Append($" data-platform=\"{platform}\"");
builder.Append($" data-product=\"{name}\"");
builder.Append($" data-version=\"{version}\"");
builder.Append($" data-build=\"{build}\">");
builder.AppendLine();
builder.AppendLine($" <td>{name}</td>");
builder.AppendLine($" <td>{version}</td>");
builder.AppendLine($" <td>{build}</td>");
builder.AppendLine(" </tr>");
}
builder.AppendLine(" </tbody>");
builder.AppendLine(" </table>");
}
if (dto.References.Count > 0)
{
builder.AppendLine(" <section>");
builder.AppendLine(" <h2>References</h2>");
foreach (var reference in dto.References
.OrderBy(r => r.Url, StringComparer.OrdinalIgnoreCase))
{
var title = reference.Title ?? string.Empty;
builder.AppendLine($" <a href=\"{Escape(reference.Url)}\">{Escape(title)}</a>");
}
builder.AppendLine(" </section>");
}
builder.AppendLine("</article>");
builder.AppendLine("</body>");
builder.AppendLine("</html>");
return builder.ToString();
}
private static void WriteFixture(string name, string contents)
{
var root = ResolveFixtureRoot();
Directory.CreateDirectory(root);
var normalized = NormalizeLineEndings(contents);
var sourcePath = Path.Combine(root, name);
File.WriteAllText(sourcePath, normalized);
var outputPath = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Apple", "Fixtures", name);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
File.WriteAllText(outputPath, normalized);
Console.WriteLine($"[AppleFixtures] Wrote {name}");
}
private static string ResolveFixtureRoot()
{
var baseDir = AppContext.BaseDirectory;
// bin/Debug/net10.0/ -> project -> src -> repo root
var root = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", ".."));
return Path.Combine(root, "src", "StellaOps.Concelier.Connector.Vndr.Apple.Tests", "Apple", "Fixtures");
}
private static string NormalizeLineEndings(string value)
=> value.Replace("\r\n", "\n", StringComparison.Ordinal);
private static string Escape(string value)
=> System.Net.WebUtility.HtmlEncode(value);
}
internal sealed record AppleFixtureDefinition(
string ArticleId,
Uri DetailUri,
bool? ForceRapidSecurityResponse = null,
IReadOnlyList<AppleFixtureProduct>? Products = null)
{
public string HtmlFixtureName => $"{ArticleId}.html";
public string ExpectedFixtureName => $"{ArticleId}.expected.json";
}
internal sealed record AppleFixtureProduct(
string Platform,
string Name,
string Version,
string Build);

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using StellaOps.Concelier.Connector.Vndr.Apple.Internal;
using StellaOps.Concelier.Connector.Vndr.Apple.Tests.Apple;
using Xunit;
namespace StellaOps.Concelier.Connector.Vndr.Apple.Tests;
public sealed class AppleLiveRegressionTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
public static IEnumerable<object[]> FixtureCases
{
get
{
foreach (var definition in AppleFixtureManager.Fixtures)
{
yield return new object[] { definition.ArticleId };
}
}
}
[Theory]
[MemberData(nameof(FixtureCases))]
public async Task Parser_SanitizedFixture_MatchesExpectedDto(string articleId)
{
var updateFlag = Environment.GetEnvironmentVariable("UPDATE_APPLE_FIXTURES");
if (!string.IsNullOrEmpty(updateFlag))
{
Console.WriteLine($"[AppleFixtures] UPDATE_APPLE_FIXTURES={updateFlag}");
}
await AppleFixtureManager.EnsureUpdatedAsync();
var expected = AppleFixtureManager.ReadExpectedDto(articleId);
var html = AppleFixtureManager.ReadFixture($"{articleId}.html");
var entry = new AppleIndexEntry(
UpdateId: articleId,
ArticleId: articleId,
Title: expected.Title,
PostingDate: expected.Published,
DetailUri: new Uri($"https://support.apple.com/en-us/{articleId}"),
Products: Array.Empty<AppleIndexProduct>(),
IsRapidSecurityResponse: expected.RapidSecurityResponse);
var dto = AppleDetailParser.Parse(html, entry);
var actualJson = JsonSerializer.Serialize(dto, SerializerOptions);
var expectedJson = JsonSerializer.Serialize(expected, SerializerOptions);
Assert.Equal(expectedJson, actualJson);
}
}

View File

@@ -0,0 +1,51 @@
{
"advisoryId": "106355",
"articleId": "106355",
"title": "About the security content of Rapid Security Responses for macOS Ventura 13.4.1",
"summary": "This document describes the content of Rapid Security Responses.",
"published": "2025-10-12T13:19:10.4446382+00:00",
"cveIds": [
"CVE-2023-37450"
],
"affected": [
{
"platform": "macOS Ventura",
"name": "macOS Ventura",
"version": "",
"build": "22F400"
},
{
"platform": "macOS Ventura",
"name": "macOS Ventura (Intel)",
"version": "",
"build": "22F400a"
}
],
"references": [
{
"url": "https://support.apple.com/103190",
"title": "Contact the vendor",
"kind": "advisory"
},
{
"url": "https://support.apple.com/en-us/106355/localeselector",
"title": "United States",
"kind": "advisory"
},
{
"url": "https://support.apple.com/kb/HT201222",
"title": "Apple security releases",
"kind": "advisory"
},
{
"url": "https://support.apple.com/kb/HT201224",
"title": "Rapid Security Responses",
"kind": "advisory"
},
{
"url": "https://www.cve.org/About/Overview",
"title": "CVE-ID"
}
],
"rapidSecurityResponse": true
}

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<body>
<article>
<h1 data-testid="update-title">About the security content of Rapid Security Responses for macOS Ventura 13.4.1</h1>
<p data-testid="update-summary">This document describes the content of Rapid Security Responses.</p>
<time data-testid="published" datetime="2025-10-12T13:19:10.4446382+00:00">October 12, 2025</time>
<section data-component="security-update">
<h2>Security Issues</h2>
<ul data-testid="cve-list">
<li>CVE-2023-37450</li>
</ul>
</section>
<table>
<tbody>
<tr data-testid="product-row" data-platform="macOS Ventura" data-product="macOS Ventura" data-version="" data-build="22F400">
<td>macOS Ventura</td>
<td></td>
<td>22F400</td>
</tr>
<tr data-testid="product-row" data-platform="macOS Ventura" data-product="macOS Ventura (Intel)" data-version="" data-build="22F400a">
<td>macOS Ventura (Intel)</td>
<td></td>
<td>22F400a</td>
</tr>
</tbody>
</table>
<section>
<h2>References</h2>
<a href="https://support.apple.com/103190">Contact the vendor</a>
<a href="https://support.apple.com/en-us/106355/localeselector">United States</a>
<a href="https://support.apple.com/kb/HT201222">Apple security releases</a>
<a href="https://support.apple.com/kb/HT201224">Rapid Security Responses</a>
<a href="https://www.cve.org/About/Overview">CVE-ID</a>
</section>
</article>
</body>
</html>

View File

@@ -0,0 +1,57 @@
{
"advisoryId": "125326",
"articleId": "125326",
"title": "About the security content of iOS 26.0.1 and iPadOS 26.0.1",
"summary": "This document describes the security content of iOS 26.0.1 and iPadOS 26.0.1.",
"published": "2025-10-12T13:19:09.732004+00:00",
"cveIds": [
"CVE-2025-43400"
],
"affected": [
{
"platform": "iOS",
"name": "iPhone 16 Pro",
"version": "26.0.1",
"build": "24A341"
},
{
"platform": "iOS",
"name": "iPhone 16 Pro",
"version": "26.0.1 (a)",
"build": "24A341a"
},
{
"platform": "iPadOS",
"name": "iPad Pro (M4)",
"version": "26",
"build": "24B120"
}
],
"references": [
{
"url": "https://support.apple.com/103190",
"title": "Contact the vendor",
"kind": "advisory"
},
{
"url": "https://support.apple.com/en-us/100100",
"title": "Apple security releases",
"kind": "advisory"
},
{
"url": "https://support.apple.com/en-us/102549",
"title": "Apple Product Security",
"kind": "advisory"
},
{
"url": "https://support.apple.com/en-us/125326/localeselector",
"title": "United States",
"kind": "advisory"
},
{
"url": "https://www.cve.org/About/Overview",
"title": "CVE-ID"
}
],
"rapidSecurityResponse": false
}

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<body>
<article>
<h1 data-testid="update-title">About the security content of iOS 26.0.1 and iPadOS 26.0.1</h1>
<p data-testid="update-summary">This document describes the security content of iOS 26.0.1 and iPadOS 26.0.1.</p>
<time data-testid="published" datetime="2025-10-12T13:19:09.7320040+00:00">October 12, 2025</time>
<section data-component="security-update">
<h2>Security Issues</h2>
<ul data-testid="cve-list">
<li>CVE-2025-43400</li>
</ul>
</section>
<table>
<tbody>
<tr data-testid="product-row" data-platform="iOS" data-product="iPhone 16 Pro" data-version="26.0.1" data-build="24A341">
<td>iPhone 16 Pro</td>
<td>26.0.1</td>
<td>24A341</td>
</tr>
<tr data-testid="product-row" data-platform="iOS" data-product="iPhone 16 Pro" data-version="26.0.1 (a)" data-build="24A341a">
<td>iPhone 16 Pro</td>
<td>26.0.1 (a)</td>
<td>24A341a</td>
</tr>
<tr data-testid="product-row" data-platform="iPadOS" data-product="iPad Pro (M4)" data-version="26" data-build="24B120">
<td>iPad Pro (M4)</td>
<td>26</td>
<td>24B120</td>
</tr>
</tbody>
</table>
<section>
<h2>References</h2>
<a href="https://support.apple.com/103190">Contact the vendor</a>
<a href="https://support.apple.com/en-us/100100">Apple security releases</a>
<a href="https://support.apple.com/en-us/102549">Apple Product Security</a>
<a href="https://support.apple.com/en-us/125326/localeselector">United States</a>
<a href="https://www.cve.org/About/Overview">CVE-ID</a>
</section>
</article>
</body>
</html>

View File

@@ -0,0 +1,51 @@
{
"advisoryId": "125328",
"articleId": "125328",
"title": "About the security content of macOS Tahoe 26.0.1",
"summary": "This document describes the security content of macOS Tahoe 26.0.1.",
"published": "2025-10-12T13:19:10.3703446+00:00",
"cveIds": [
"CVE-2025-43400"
],
"affected": [
{
"platform": "macOS",
"name": "MacBook Pro (M4)",
"version": "26.0.1",
"build": "26A123"
},
{
"platform": "macOS",
"name": "Mac Studio",
"version": "26",
"build": "26A120b"
}
],
"references": [
{
"url": "https://support.apple.com/103190",
"title": "Contact the vendor",
"kind": "advisory"
},
{
"url": "https://support.apple.com/en-us/100100",
"title": "Apple security releases",
"kind": "advisory"
},
{
"url": "https://support.apple.com/en-us/102549",
"title": "Apple Product Security",
"kind": "advisory"
},
{
"url": "https://support.apple.com/en-us/125328/localeselector",
"title": "United States",
"kind": "advisory"
},
{
"url": "https://www.cve.org/About/Overview",
"title": "CVE-ID"
}
],
"rapidSecurityResponse": false
}

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<body>
<article>
<h1 data-testid="update-title">About the security content of macOS Tahoe 26.0.1</h1>
<p data-testid="update-summary">This document describes the security content of macOS Tahoe 26.0.1.</p>
<time data-testid="published" datetime="2025-10-12T13:19:10.3703446+00:00">October 12, 2025</time>
<section data-component="security-update">
<h2>Security Issues</h2>
<ul data-testid="cve-list">
<li>CVE-2025-43400</li>
</ul>
</section>
<table>
<tbody>
<tr data-testid="product-row" data-platform="macOS" data-product="Mac Studio" data-version="26" data-build="26A120b">
<td>Mac Studio</td>
<td>26</td>
<td>26A120b</td>
</tr>
<tr data-testid="product-row" data-platform="macOS" data-product="MacBook Pro (M4)" data-version="26.0.1" data-build="26A123">
<td>MacBook Pro (M4)</td>
<td>26.0.1</td>
<td>26A123</td>
</tr>
</tbody>
</table>
<section>
<h2>References</h2>
<a href="https://support.apple.com/103190">Contact the vendor</a>
<a href="https://support.apple.com/en-us/100100">Apple security releases</a>
<a href="https://support.apple.com/en-us/102549">Apple Product Security</a>
<a href="https://support.apple.com/en-us/125328/localeselector">United States</a>
<a href="https://www.cve.org/About/Overview">CVE-ID</a>
</section>
</article>
</body>
</html>

View File

@@ -0,0 +1,61 @@
{
"advisoryId": "HT214108",
"articleId": "HT214108",
"title": "About the security content of visionOS 1.2",
"summary": "This document describes the security content of visionOS 1.2.",
"published": "2025-10-12T13:19:10.5262006+00:00",
"cveIds": [
"CVE-2024-27800",
"CVE-2024-27801",
"CVE-2024-27802",
"CVE-2024-27808",
"CVE-2024-27811",
"CVE-2024-27812",
"CVE-2024-27815",
"CVE-2024-27817",
"CVE-2024-27820",
"CVE-2024-27828",
"CVE-2024-27830",
"CVE-2024-27831",
"CVE-2024-27832",
"CVE-2024-27833",
"CVE-2024-27836",
"CVE-2024-27838",
"CVE-2024-27840",
"CVE-2024-27844",
"CVE-2024-27850",
"CVE-2024-27851",
"CVE-2024-27856",
"CVE-2024-27857",
"CVE-2024-27884",
"CVE-2024-40771"
],
"affected": [],
"references": [
{
"url": "https://support.apple.com/103190",
"title": "Contact the vendor",
"kind": "advisory"
},
{
"url": "https://support.apple.com/en-us/120906/localeselector",
"title": "United States",
"kind": "advisory"
},
{
"url": "https://support.apple.com/kb/HT201220",
"title": "Apple Product Security",
"kind": "advisory"
},
{
"url": "https://support.apple.com/kb/HT201222",
"title": "Apple security releases",
"kind": "advisory"
},
{
"url": "https://www.cve.org/About/Overview",
"title": "CVE-ID"
}
],
"rapidSecurityResponse": false
}

View File

@@ -0,0 +1,20 @@
{
"advisoryId": "HT215500",
"articleId": "HT215500",
"title": "Rapid Security Response iOS 18.0.1 (c)",
"summary": "Rapid Security Response provides important security fixes between software updates.",
"published": "2025-10-02T15:30:00+00:00",
"cveIds": [
"CVE-2025-2468"
],
"affected": [
{
"platform": "iOS",
"name": "iPhone 15 Pro",
"version": "18.0.1 (c)",
"build": "22A123c"
}
],
"references": [],
"rapidSecurityResponse": true
}

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<body>
<article>
<h1 data-testid="update-title">About the security content of visionOS 1.2</h1>
<p data-testid="update-summary">This document describes the security content of visionOS 1.2.</p>
<time data-testid="published" datetime="2025-10-12T13:19:10.5262006+00:00">October 12, 2025</time>
<section data-component="security-update">
<h2>Security Issues</h2>
<ul data-testid="cve-list">
<li>CVE-2024-27800</li>
<li>CVE-2024-27801</li>
<li>CVE-2024-27802</li>
<li>CVE-2024-27808</li>
<li>CVE-2024-27811</li>
<li>CVE-2024-27812</li>
<li>CVE-2024-27815</li>
<li>CVE-2024-27817</li>
<li>CVE-2024-27820</li>
<li>CVE-2024-27828</li>
<li>CVE-2024-27830</li>
<li>CVE-2024-27831</li>
<li>CVE-2024-27832</li>
<li>CVE-2024-27833</li>
<li>CVE-2024-27836</li>
<li>CVE-2024-27838</li>
<li>CVE-2024-27840</li>
<li>CVE-2024-27844</li>
<li>CVE-2024-27850</li>
<li>CVE-2024-27851</li>
<li>CVE-2024-27856</li>
<li>CVE-2024-27857</li>
<li>CVE-2024-27884</li>
<li>CVE-2024-40771</li>
</ul>
</section>
<section>
<h2>References</h2>
<a href="https://support.apple.com/103190">Contact the vendor</a>
<a href="https://support.apple.com/en-us/120906/localeselector">United States</a>
<a href="https://support.apple.com/kb/HT201220">Apple Product Security</a>
<a href="https://support.apple.com/kb/HT201222">Apple security releases</a>
<a href="https://www.cve.org/About/Overview">CVE-ID</a>
</section>
</article>
</body>
</html>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<body>
<article>
<h1 data-testid="update-title">Rapid Security Response iOS 18.0.1 (c)</h1>
<p data-testid="update-summary">Rapid Security Response provides important security fixes between software updates.</p>
<time data-testid="published" datetime="2025-10-02T15:30:00+00:00">October 2, 2025</time>
<section data-component="security-update">
<h2>Security Issues</h2>
<ul data-testid="cve-list">
<li>CVE-2025-2468</li>
</ul>
</section>
<table>
<tbody>
<tr data-testid="product-row" data-platform="iOS" data-product="iPhone 15 Pro" data-version="18.0.1 (c)" data-build="22A123c">
<td>iPhone 15 Pro</td>
<td>18.0.1 (c)</td>
<td>22A123c</td>
</tr>
</tbody>
</table>
</article>
</body>
</html>

View File

@@ -0,0 +1,101 @@
{
"updates": [
{
"id": "125326",
"articleId": "125326",
"title": "About the security content of iOS 26.0.1 and iPadOS 26.0.1",
"postingDate": "2025-10-12T13:19:09.7320040+00:00",
"detailUrl": "https://support.example.com/en-us/125326",
"rapidSecurityResponse": false,
"products": [
{
"platform": "iPadOS",
"name": "iPad Pro (M4)",
"version": "26",
"build": "24B120"
},
{
"platform": "iOS",
"name": "iPhone 16 Pro",
"version": "26.0.1",
"build": "24A341"
},
{
"platform": "iOS",
"name": "iPhone 16 Pro",
"version": "26.0.1 (a)",
"build": "24A341a"
}
]
},
{
"id": "125328",
"articleId": "125328",
"title": "About the security content of macOS Tahoe 26.0.1",
"postingDate": "2025-10-12T13:19:10.3703446+00:00",
"detailUrl": "https://support.example.com/en-us/125328",
"rapidSecurityResponse": false,
"products": [
{
"platform": "macOS",
"name": "Mac Studio",
"version": "26",
"build": "26A120b"
},
{
"platform": "macOS",
"name": "MacBook Pro (M4)",
"version": "26.0.1",
"build": "26A123"
}
]
},
{
"id": "106355",
"articleId": "106355",
"title": "About the security content of Rapid Security Responses for macOS Ventura 13.4.1",
"postingDate": "2025-10-12T13:19:10.4446382+00:00",
"detailUrl": "https://support.example.com/en-us/106355",
"rapidSecurityResponse": true,
"products": [
{
"platform": "macOS Ventura",
"name": "macOS Ventura",
"version": "",
"build": "22F400"
},
{
"platform": "macOS Ventura",
"name": "macOS Ventura (Intel)",
"version": "",
"build": "22F400a"
}
]
},
{
"id": "HT214108",
"articleId": "HT214108",
"title": "About the security content of visionOS 1.2",
"postingDate": "2025-10-12T13:19:10.5262006+00:00",
"detailUrl": "https://support.example.com/en-us/HT214108",
"rapidSecurityResponse": false,
"products": []
},
{
"id": "RSR-iOS-18.0.1-c",
"articleId": "HT215500",
"title": "Rapid Security Response iOS 18.0.1 (c)",
"postingDate": "2025-10-02T15:30:00+00:00",
"detailUrl": "https://support.example.com/en-us/HT215500",
"rapidSecurityResponse": true,
"products": [
{
"platform": "iOS",
"name": "iPhone 15 Pro",
"version": "18.0.1 (c)",
"build": "22A123c"
}
]
}
]
}

View File

@@ -0,0 +1,19 @@
<?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.Vndr.Apple/StellaOps.Concelier.Connector.Vndr.Apple.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Apple/Fixtures/*.html" CopyToOutputDirectory="Always" TargetPath="Source/Vndr/Apple/Fixtures/%(Filename)%(Extension)" />
<None Include="Apple/Fixtures/*.json" CopyToOutputDirectory="Always" TargetPath="Source/Vndr/Apple/Fixtures/%(Filename)%(Extension)" />
</ItemGroup>
</Project>