|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Deployment.DotNet.Releases;
namespace Microsoft.DotNet.Cli.SdkVulnerability;
/// <summary>
/// Checks an SDK version against release metadata for vulnerabilities and EOL status.
/// Pure logic — no I/O or network calls.
/// </summary>
internal static class SdkVulnerabilityChecker
{
/// <summary>
/// Checks the given SDK version against the product collection and release data.
/// </summary>
/// <param name="sdkVersionString">The resolved SDK version string (e.g. "9.0.300").</param>
/// <param name="productCollection">The product collection from releases-index.json.</param>
/// <param name="getProductReleases">A function to get releases for a product (may read from cache).</param>
/// <returns>Vulnerability info, or null if the SDK version cannot be matched to release data.</returns>
public static SdkVulnerabilityInfo? Check(
string sdkVersionString,
ProductCollection productCollection,
Func<Product, IEnumerable<ProductRelease>> getProductReleases)
{
if (!ReleaseVersion.TryParse(sdkVersionString, out ReleaseVersion? sdkVersion))
{
return null;
}
// Find the product channel for this SDK version (e.g. "9.0" for SDK "9.0.300")
string channelVersion = $"{sdkVersion.Major}.{sdkVersion.Minor}";
Product? product = productCollection
.FirstOrDefault(p => p.ProductVersion.Equals(channelVersion));
if (product is null)
{
return null;
}
bool isEol = product.IsOutOfSupport();
DateTime? eolDate = product.EndOfLifeDate;
// Get all releases for this channel to find CVEs
IEnumerable<ProductRelease> releases = getProductReleases(product);
List<ProductRelease> releaseList = releases.ToList();
if (releaseList.Count == 0)
{
// No release data available — can still report EOL status
return new SdkVulnerabilityInfo
{
IsEol = isEol,
EolDate = eolDate,
};
}
// Find which release contains this SDK version
int currentReleaseIndex = -1;
for (int i = 0; i < releaseList.Count; i++)
{
if (releaseList[i].Sdks.Any(sdk => sdk.Version == sdkVersion))
{
currentReleaseIndex = i;
break;
}
}
// If the SDK version isn't present in any release, we can't determine
// its vulnerability status — return null per the documented contract.
if (currentReleaseIndex < 0)
{
return null;
}
// Collect CVEs from all releases newer than the current one.
// Releases are ordered newest-first from the API.
// All CVEs in releases after our SDK's release are vulnerabilities that affect us.
var cves = new List<SdkCveInfo>();
if (currentReleaseIndex > 0)
{
for (int i = 0; i < currentReleaseIndex; i++)
{
foreach (var cve in releaseList[i].Cves)
{
cves.Add(new SdkCveInfo
{
Id = cve.Id,
Url = cve.DescriptionLink?.ToString() ?? $"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve.Id}"
});
}
}
}
// Deduplicate CVEs by ID (same CVE can appear in multiple releases)
cves = cves.DistinctBy(c => c.Id).ToList();
// Find the latest SDK version, preferring the same feature band.
// If the user's band has no newer SDK but a newer SDK exists in a different
// band on the channel, recommend that and flag the band as discontinued.
string? latestInBand = FindLatestSdkInFeatureBand(sdkVersion, product, releaseList);
string? recommendedVersion = latestInBand;
bool featureBandDiscontinued = false;
if (latestInBand is null
&& product.LatestSdkVersion is not null
&& product.LatestSdkVersion > sdkVersion)
{
recommendedVersion = product.LatestSdkVersion.ToString();
featureBandDiscontinued = true;
}
return new SdkVulnerabilityInfo
{
IsEol = isEol,
EolDate = eolDate,
Cves = cves,
LatestSdkVersion = recommendedVersion,
FeatureBandDiscontinued = featureBandDiscontinued,
};
}
private static string? FindLatestSdkInFeatureBand(
ReleaseVersion sdkVersion,
Product product,
List<ProductRelease> releases)
{
// If the product's latest SDK is in the same feature band, use it directly
if (product.LatestSdkVersion?.SdkFeatureBand == sdkVersion.SdkFeatureBand
&& product.LatestSdkVersion > sdkVersion)
{
return product.LatestSdkVersion.ToString();
}
// Otherwise, search through releases for the newest SDK in our feature band
ReleaseVersion? latest = null;
foreach (var release in releases)
{
foreach (var sdk in release.Sdks)
{
if (sdk.Version.SdkFeatureBand == sdkVersion.SdkFeatureBand
&& sdk.Version > sdkVersion
&& (latest is null || sdk.Version > latest))
{
latest = sdk.Version;
}
}
}
return latest?.ToString();
}
}
|