File: SdkVulnerability\SdkVulnerabilityChecker.cs
Web Access
Project: src\src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet)
// 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();
    }
}