| File: CheckSdkVulnerabilities.cs | Web Access |
| Project: 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 { }