File: CheckSdkVulnerabilities.cs
Web Access
Project: src\src\sdk\src\Tasks\Microsoft.NET.Build.Tasks\Microsoft.NET.Build.Tasks.csproj (Microsoft.NET.Build.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Configurer;

namespace Microsoft.NET.Build.Tasks;

/// <summary>
/// MSBuild task that reads cached SDK vulnerability/EOL data and emits
/// NETSDK1238 (vulnerabilities), NETSDK1239 (EOL), and NETSDK1240 (feature band
/// discontinued) warnings. The cache is populated by the CLI during restore
/// (background, no blocking). This task only reads from disk — it never makes
/// network calls.
/// </summary>
public class CheckSdkVulnerabilities : TaskBase
{
    private const string CacheDirectoryName = "sdk-vulnerability-cache";
    private const string SummaryFilePrefix = "sdk-status-";

    [Required]
    public string SdkVersion { get; set; } = string.Empty;

    protected override void ExecuteCore()
    {
        if (bool.TryParse(Environment.GetEnvironmentVariable(Microsoft.DotNet.Cli.EnvironmentVariableNames.SDK_VULNERABILITY_CHECK_DISABLE), out bool disabled) && disabled)
        {
            return;
        }

        string? userProfileDir = new CliFolderPathCalculatorCore().GetDotnetUserProfileFolderPath();
        if (string.IsNullOrEmpty(userProfileDir))
        {
            return;
        }

        // Validate version to prevent path traversal via malformed version strings
        if (SdkVersion.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
        {
            return;
        }

        string summaryPath = Path.Combine(userProfileDir, CacheDirectoryName, $"{SummaryFilePrefix}{SdkVersion}.json");
        if (!File.Exists(summaryPath))
        {
            return;
        }

        SdkVulnerabilitySummary? summary;
        try
        {
            string json = File.ReadAllText(summaryPath);
            summary = JsonSerializer.Deserialize(json, SdkVulnerabilitySummaryContext.Default.SdkVulnerabilitySummary);
        }
        catch
        {
            // Corrupt or partially-written cache — skip silently
            return;
        }

        if (summary is null)
        {
            return;
        }

        if (summary.IsEol && summary.EolDate.HasValue)
        {
            Log.LogWarning(
                Strings.SdkVersionIsEol,
                SdkVersion,
                summary.EolDate.Value.ToString("yyyy-MM-dd"));
        }

        if (summary.Cves is { Count: > 0 })
        {
            string cveIds = string.Join(", ", summary.Cves.Where(c => c.Id is not null).Select(c => c.Id));
            if (!string.IsNullOrEmpty(cveIds))
            {
                string upgradeSuffix = summary.LatestSdkVersion is not null
                    ? $" {string.Format(Strings.SdkVersionUpdateRecommendation_Info, summary.LatestSdkVersion)}"
                    : string.Empty;
                Log.LogWarning(
                    Strings.SdkVersionHasVulnerabilities,
                    SdkVersion,
                    cveIds,
                    upgradeSuffix);
            }
        }

        if (summary.FeatureBandDiscontinued && !string.IsNullOrEmpty(summary.LatestSdkVersion))
        {
            Log.LogWarning(
                Strings.SdkFeatureBandDiscontinued,
                SdkVersion,
                summary.LatestSdkVersion);
        }
    }

    // Local DTOs for deserializing the cached summary.
    // Must stay serialization-compatible with SdkVulnerabilityInfo written by the CLI cache.
    internal sealed class SdkVulnerabilitySummary
    {
        public bool IsEol { get; set; }
        public DateTime? EolDate { get; set; }
        public List<SdkCveSummary>? Cves { get; set; }
        public string? LatestSdkVersion { get; set; }
        public bool FeatureBandDiscontinued { get; set; }
    }

    internal sealed class SdkCveSummary
    {
        public string? Id { get; set; }
        public string? Url { get; set; }
    }
}

[JsonSerializable(typeof(CheckSdkVulnerabilities.SdkVulnerabilitySummary))]
internal partial class SdkVulnerabilitySummaryContext : JsonSerializerContext
{
}