diff --git a/src/NuGet.Clients/NuGet.CommandLine/Commands/InstallCommand.cs b/src/NuGet.Clients/NuGet.CommandLine/Commands/InstallCommand.cs index 96a706779e7..8180679cb13 100644 --- a/src/NuGet.Clients/NuGet.CommandLine/Commands/InstallCommand.cs +++ b/src/NuGet.Clients/NuGet.CommandLine/Commands/InstallCommand.cs @@ -190,6 +190,8 @@ private async Task PerformV2RestoreAsync(string packagesConfigFilePath, string i packageRestoreFailedEvent: (sender, args) => { failedEvents.Enqueue(args); }, sourceRepositories: packageSources.Select(sourceRepositoryProvider.CreateRepository), maxNumberOfParallelTasks: DisableParallelProcessing ? 1 : PackageManagementConstants.DefaultMaxDegreeOfParallelism, + enableNuGetAudit: true, + restoreAuditProperties: new(), logger: Console); var packageSaveMode = Packaging.PackageSaveMode.Defaultv2; diff --git a/src/NuGet.Clients/NuGet.CommandLine/Commands/RestoreCommand.cs b/src/NuGet.Clients/NuGet.CommandLine/Commands/RestoreCommand.cs index 462cbdc6e11..28cfcd8c17e 100644 --- a/src/NuGet.Clients/NuGet.CommandLine/Commands/RestoreCommand.cs +++ b/src/NuGet.Clients/NuGet.CommandLine/Commands/RestoreCommand.cs @@ -9,7 +9,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Runtime.Remoting; using System.Threading; using System.Threading.Tasks; using NuGet.Commands; @@ -273,6 +272,7 @@ private async Task> PerformNuGetV2RestoreAsync(Pac List packageRestoreData = new(); bool areAnyPackagesMissing = false; + Dictionary restoreAuditProperties = null; if (packageRestoreInputs.RestoringWithSolutionFile) { @@ -283,7 +283,7 @@ private async Task> PerformNuGetV2RestoreAsync(Pac { foreach (PackageReference packageReference in GetInstalledPackageReferences(configFile)) { - if (configToProjectPath.TryGetValue(configFile, out string projectPath)) + if (!configToProjectPath.TryGetValue(configFile, out string projectPath)) { projectPath = configFile; } @@ -303,6 +303,7 @@ private async Task> PerformNuGetV2RestoreAsync(Pac packageRestoreData.Add(new PackageRestoreData(package.Key, package.Value, !exists)); areAnyPackagesMissing |= !exists; } + restoreAuditProperties = GetRestoreAuditProperties(packageRestoreInputs); } else if (packageRestoreInputs.PackagesConfigFiles.Count > 0) @@ -324,15 +325,34 @@ private async Task> PerformNuGetV2RestoreAsync(Pac throw new InvalidOperationException(message); } + restoreAuditProperties = new(PathUtility.GetStringComparerBasedOnOS()); + + string referenceFile = packageReferenceFile; + // If restoring with a csproj directly, ensure we read the audit configuration. + if (packageRestoreInputs.ProjectFiles.Count > 0) + { + var packageSpec = packageRestoreInputs.ProjectReferenceLookup.GetProjectSpec(packageRestoreInputs.ProjectFiles.First()); + if (packageSpec != null) + { + referenceFile = packageSpec.FilePath; + restoreAuditProperties.Add(referenceFile, packageSpec.RestoreMetadata.RestoreAuditProperties); + } + } foreach (PackageReference packageReference in GetInstalledPackageReferences(packageReferenceFile)) { bool exists = nuGetPackageManager.PackageExistsInPackagesFolder(packageReference.PackageIdentity, packageSaveMode); - packageRestoreData.Add(new PackageRestoreData(packageReference, [packageReferenceFile], !exists)); + packageRestoreData.Add(new PackageRestoreData(packageReference, [referenceFile], !exists)); areAnyPackagesMissing |= !exists; } } + var packageSources = GetPackageSources(Settings); + + var repositories = packageSources + .Select(sourceRepositoryProvider.CreateRepository) + .ToList(); + if (!areAnyPackagesMissing) { var message = string.Format( @@ -350,6 +370,15 @@ private async Task> PerformNuGetV2RestoreAsync(Pac packagesFolderPath, restoreSummaries); + using SourceCacheContext cacheContext = new(); + + var auditUtility = new AuditChecker( + repositories, + cacheContext, + Console); + + await auditUtility.CheckPackageVulnerabilitiesAsync(packageRestoreData, restoreAuditProperties, CancellationToken.None); + if (restoreSummaries.Count == 0) { restoreSummaries.Add(new RestoreSummary(success: true)); @@ -358,12 +387,6 @@ private async Task> PerformNuGetV2RestoreAsync(Pac return restoreSummaries; } - var packageSources = GetPackageSources(Settings); - - var repositories = packageSources - .Select(sourceRepositoryProvider.CreateRepository) - .ToArray(); - var installCount = 0; var failedEvents = new ConcurrentQueue(); var collectorLogger = new RestoreCollectorLogger(Console); @@ -378,6 +401,8 @@ private async Task> PerformNuGetV2RestoreAsync(Pac maxNumberOfParallelTasks: DisableParallelProcessing ? 1 : PackageManagementConstants.DefaultMaxDegreeOfParallelism, + enableNuGetAudit: true, + restoreAuditProperties, logger: collectorLogger); CheckRequireConsent(); @@ -458,6 +483,20 @@ private static Dictionary GetPackagesConfigToProjectPath(Package return configToProjectPath; } + private static Dictionary GetRestoreAuditProperties(PackageRestoreInputs packageRestoreInputs) + { + Dictionary restoreAuditProperties = new(PathUtility.GetStringComparerBasedOnOS()); + foreach (PackageSpec project in packageRestoreInputs.ProjectReferenceLookup.Projects) + { + if (project.RestoreMetadata?.ProjectStyle == ProjectStyle.PackagesConfig) + { + restoreAuditProperties.Add(project.FilePath, project.RestoreMetadata.RestoreAuditProperties); + } + } + + return restoreAuditProperties; + } + /// /// Processes List of PackageRestoreFailedEventArgs into a List of RestoreLogMessages. /// diff --git a/src/NuGet.Core/NuGet.Build.Tasks.Console/MSBuildStaticGraphRestore.cs b/src/NuGet.Core/NuGet.Build.Tasks.Console/MSBuildStaticGraphRestore.cs index a15a8aee6e2..d78c07ab4f9 100644 --- a/src/NuGet.Core/NuGet.Build.Tasks.Console/MSBuildStaticGraphRestore.cs +++ b/src/NuGet.Core/NuGet.Build.Tasks.Console/MSBuildStaticGraphRestore.cs @@ -849,7 +849,8 @@ private PackageSpec GetPackageSpec(IMSBuildProject project, IReadOnlyDictionary< restoreMetadata = new PackagesConfigProjectRestoreMetadata { PackagesConfigPath = packagesConfigFilePath, - RepositoryPath = GetRepositoryPath(project, settings) + RepositoryPath = GetRepositoryPath(project, settings), + RestoreAuditProperties = auditProperties, }; } else diff --git a/src/NuGet.Core/NuGet.Build.Tasks/BuildTasksUtility.cs b/src/NuGet.Core/NuGet.Build.Tasks/BuildTasksUtility.cs index 45d623a6db5..d562d528aee 100644 --- a/src/NuGet.Core/NuGet.Build.Tasks/BuildTasksUtility.cs +++ b/src/NuGet.Core/NuGet.Build.Tasks/BuildTasksUtility.cs @@ -31,6 +31,7 @@ using NuGet.Shared; using static NuGet.Shared.XmlUtility; using System.Globalization; +using System.Collections; #endif namespace NuGet.Build.Tasks @@ -422,6 +423,7 @@ private static async Task PerformNuGetV2RestoreAsync(Common.ILog ISettings settings = null; Dictionary> packageReferenceToProjects = new(PackageReferenceComparer.Instance); + Dictionary restoreAuditProperties = new(PathUtility.GetStringComparerBasedOnOS()); foreach (PackageSpec packageSpec in dgFile.Projects.Where(i => i.RestoreMetadata.ProjectStyle == ProjectStyle.PackagesConfig)) { @@ -454,8 +456,9 @@ private static async Task PerformNuGetV2RestoreAsync(Common.ILog value ??= new(); packageReferenceToProjects.Add(packageReference, value); } - value.Add(packagesConfigPath); + value.Add(pcRestoreMetadata.PackagesConfigPath); } + restoreAuditProperties.Add(packageSpec.FilePath, packageSpec.RestoreMetadata.RestoreAuditProperties); } if (string.IsNullOrEmpty(repositoryPath)) @@ -482,12 +485,22 @@ private static async Task PerformNuGetV2RestoreAsync(Common.ILog areAnyPackagesMissing |= !exists; } + var repositories = sourceRepositoryProvider.GetRepositories().ToList(); + if (!areAnyPackagesMissing) { + using SourceCacheContext cacheContext = new(); + + var auditUtility = new AuditChecker( + repositories, + cacheContext, + log); + + await auditUtility.CheckPackageVulnerabilitiesAsync(packageRestoreData, restoreAuditProperties, CancellationToken.None); + return new RestoreSummary(true); } - var repositories = sourceRepositoryProvider.GetRepositories().ToArray(); var installCount = 0; var failedEvents = new ConcurrentQueue(); @@ -503,6 +516,8 @@ private static async Task PerformNuGetV2RestoreAsync(Common.ILog maxNumberOfParallelTasks: disableParallel ? 1 : PackageManagementConstants.DefaultMaxDegreeOfParallelism, + enableNuGetAudit: true, + restoreAuditProperties, logger: collectorLogger); // TODO: Check require consent? diff --git a/src/NuGet.Core/NuGet.Build.Tasks/NuGet.targets b/src/NuGet.Core/NuGet.Build.Tasks/NuGet.targets index 33463f79f30..90a43784355 100644 --- a/src/NuGet.Core/NuGet.Build.Tasks/NuGet.targets +++ b/src/NuGet.Core/NuGet.Build.Tasks/NuGet.targets @@ -908,6 +908,8 @@ Copyright (c) .NET Foundation. All rights reserved. $(_OutputConfigFilePaths) $(_OutputPackagesPath) @(_RestoreTargetFrameworksOutputFiltered) + $(NuGetAudit) + $(NuGetAuditLevel) diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs index 0f8088157af..7b7b73d8b35 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs @@ -305,7 +305,7 @@ await _logger.LogAsync(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1803, } bool auditEnabled = AuditUtility.ParseEnableValue( - _request.Project.RestoreMetadata?.RestoreAuditProperties?.EnableAudit, + _request.Project.RestoreMetadata?.RestoreAuditProperties, _request.Project.FilePath, _logger); telemetry.TelemetryEvent[AuditEnabled] = auditEnabled ? "enabled" : "disabled"; diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/AuditUtility.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/AuditUtility.cs index f583cc419d3..e2a56ec8576 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/AuditUtility.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/AuditUtility.cs @@ -13,6 +13,7 @@ using NuGet.DependencyResolver; using NuGet.LibraryModel; using NuGet.Packaging.Core; +using NuGet.ProjectModel; using NuGet.Protocol; using NuGet.Protocol.Model; using NuGet.Versioning; @@ -363,27 +364,15 @@ private PackageVulnerabilitySeverity ParseAuditLevel() return PackageVulnerabilitySeverity.Low; } - if (string.Equals("low", auditLevel, StringComparison.OrdinalIgnoreCase)) + if (_restoreAuditProperties!.TryParseAuditLevel(out PackageVulnerabilitySeverity result)) { - return PackageVulnerabilitySeverity.Low; - } - if (string.Equals("moderate", auditLevel, StringComparison.OrdinalIgnoreCase)) - { - return PackageVulnerabilitySeverity.Moderate; - } - if (string.Equals("high", auditLevel, StringComparison.OrdinalIgnoreCase)) - { - return PackageVulnerabilitySeverity.High; - } - if (string.Equals("critical", auditLevel, StringComparison.OrdinalIgnoreCase)) - { - return PackageVulnerabilitySeverity.Critical; + return result; } string messageText = string.Format(Strings.Error_InvalidNuGetAuditLevelValue, auditLevel, "low, moderate, high, critical"); RestoreLogMessage message = RestoreLogMessage.CreateError(NuGetLogCode.NU1014, messageText); _logger.Log(message); - return 0; + return PackageVulnerabilitySeverity.Low; } internal enum NuGetAuditMode { Unknown, Direct, All } @@ -414,27 +403,22 @@ private NuGetAuditMode ParseAuditMode() } // Enum parsing and ToString are a magnitude of times slower than a naive implementation. - public static bool ParseEnableValue(string? value, string projectFullPath, ILogger logger) + public static bool ParseEnableValue(RestoreAuditProperties? value, string projectFullPath, ILogger logger) { - // Earlier versions allowed "enable" and "default" to opt-in - if (string.IsNullOrEmpty(value) - || string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase) - || string.Equals(value, "enable", StringComparison.OrdinalIgnoreCase) - || string.Equals(value, "default", StringComparison.OrdinalIgnoreCase)) + if (value == null) { return true; } - if (string.Equals(value, bool.FalseString, StringComparison.OrdinalIgnoreCase) - || string.Equals(value, "disable", StringComparison.OrdinalIgnoreCase)) + + if (!value.TryParseEnableAudit(out bool result)) { - return false; + string messageText = string.Format(Strings.Error_InvalidNuGetAuditValue, value, "true, false"); + RestoreLogMessage message = RestoreLogMessage.CreateError(NuGetLogCode.NU1014, messageText); + message.ProjectPath = projectFullPath; + logger.Log(message); } - string messageText = string.Format(Strings.Error_InvalidNuGetAuditValue, value, "true, false"); - RestoreLogMessage message = RestoreLogMessage.CreateError(NuGetLogCode.NU1014, messageText); - message.ProjectPath = projectFullPath; - logger.Log(message); - return true; + return result; } // Enum parsing and ToString are a magnitude of times slower than a naive implementation. diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/MSBuildRestoreUtility.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/MSBuildRestoreUtility.cs index baa6d8f6aaf..2e53d498293 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/MSBuildRestoreUtility.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/MSBuildRestoreUtility.cs @@ -285,7 +285,7 @@ public static PackageSpec GetPackageSpec(IEnumerable items) ); } pcRestoreMetadata.RestoreLockProperties = GetRestoreLockProperties(specItem); - + pcRestoreMetadata.RestoreAuditProperties = GetRestoreAuditProperties(specItem); } if (restoreType == ProjectStyle.ProjectJson) diff --git a/src/NuGet.Core/NuGet.PackageManagement/Audit/AuditCheckResult.cs b/src/NuGet.Core/NuGet.PackageManagement/Audit/AuditCheckResult.cs new file mode 100644 index 00000000000..3aa9d424110 --- /dev/null +++ b/src/NuGet.Core/NuGet.PackageManagement/Audit/AuditCheckResult.cs @@ -0,0 +1,89 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using NuGet.Common; +using NuGet.Packaging.Core; + +namespace NuGet.PackageManagement +{ + public record AuditCheckResult + { + public IReadOnlyList Warnings { get; } + internal bool IsAuditEnabled { get; set; } = true; + + internal int Severity0VulnerabilitiesFound { get; set; } + internal int Severity1VulnerabilitiesFound { get; set; } + internal int Severity2VulnerabilitiesFound { get; set; } + internal int Severity3VulnerabilitiesFound { get; set; } + internal int InvalidSeverityVulnerabilitiesFound { get; set; } + internal List? Packages { get; set; } + internal double? DownloadDurationInSeconds { get; set; } + internal double? CheckPackagesDurationInSeconds { get; set; } + internal int? SourcesWithVulnerabilities { get; set; } + + private const string AuditVulnerabilitiesStatus = "PackagesConfig.Audit.Enabled"; + private const string AuditVulnerabilitiesCount = "PackagesConfig.Audit.Vulnerability.Count"; + private const string AuditVulnerabilitiesSev0Count = "PackagesConfig.Audit.Vulnerability.Severity0.Count"; + private const string AuditVulnerabilitiesSev1Count = "PackagesConfig.Audit.Vulnerability.Severity1.Count"; + private const string AuditVulnerabilitiesSev2Count = "PackagesConfig.Audit.Vulnerability.Severity2.Count"; + private const string AuditVulnerabilitiesSev3Count = "PackagesConfig.Audit.Vulnerability.Severity3.Count"; + private const string AuditVulnerabilitiesInvalidSeverityCount = "PackagesConfig.Audit.Vulnerability.SeverityInvalid.Count"; + private const string AuditDurationDownload = "PackagesConfig.Audit.Duration.Download"; + private const string AuditDurationCheck = "PackagesConfig.Audit.Duration.Check"; + private const string SourcesWithVulnerabilitiesCount = "PackagesConfig.Audit.DataSources.Count"; + private const string AuditVulnerabilitiesPackages = "PackagesConfig.Audit.Vulnerability.Packages"; + + + internal AuditCheckResult(IReadOnlyList warnings) + { + if (warnings is null) + { + throw new ArgumentNullException(nameof(warnings)); + } + + Warnings = warnings; + } + + public void AddMetricsToTelemetry(TelemetryEvent telemetryEvent) + { + telemetryEvent[AuditVulnerabilitiesStatus] = IsAuditEnabled; + telemetryEvent[AuditVulnerabilitiesSev0Count] = Severity0VulnerabilitiesFound; + telemetryEvent[AuditVulnerabilitiesSev1Count] = Severity1VulnerabilitiesFound; + telemetryEvent[AuditVulnerabilitiesSev2Count] = Severity2VulnerabilitiesFound; + telemetryEvent[AuditVulnerabilitiesSev3Count] = Severity3VulnerabilitiesFound; + telemetryEvent[AuditVulnerabilitiesInvalidSeverityCount] = InvalidSeverityVulnerabilitiesFound; + telemetryEvent[AuditVulnerabilitiesCount] = Packages?.Count ?? 0; + + if (DownloadDurationInSeconds.HasValue) + { + telemetryEvent[AuditDurationDownload] = DownloadDurationInSeconds; + } + if (CheckPackagesDurationInSeconds.HasValue) + { + telemetryEvent[AuditDurationCheck] = CheckPackagesDurationInSeconds; + } + + if (SourcesWithVulnerabilities.HasValue) + { + telemetryEvent[SourcesWithVulnerabilitiesCount] = SourcesWithVulnerabilities; + } + + if (Packages is not null) + { + List result = new List(Packages.Count); + foreach (var package in Packages) + { + TelemetryEvent packageData = new TelemetryEvent(eventName: string.Empty); + packageData.AddPiiData("id", package.Id.ToLowerInvariant()); + packageData["version"] = package.Version; + result.Add(packageData); + } + telemetryEvent.ComplexData[AuditVulnerabilitiesPackages] = result; + } + } + } +} diff --git a/src/NuGet.Core/NuGet.PackageManagement/AuditUtility.cs b/src/NuGet.Core/NuGet.PackageManagement/Audit/AuditChecker.cs similarity index 53% rename from src/NuGet.Core/NuGet.PackageManagement/AuditUtility.cs rename to src/NuGet.Core/NuGet.PackageManagement/Audit/AuditChecker.cs index 4ceab987c69..d1ed62f3b20 100644 --- a/src/NuGet.Core/NuGet.PackageManagement/AuditUtility.cs +++ b/src/NuGet.Core/NuGet.PackageManagement/Audit/AuditChecker.cs @@ -14,34 +14,50 @@ using NuGet.Protocol.Model; using NuGet.Versioning; using NuGet.Protocol.Core.Types; +using System.Diagnostics; +using NuGet.ProjectModel; +using NuGet.Shared; namespace NuGet.PackageManagement { - internal class AuditUtility + public class AuditChecker( + List sourceRepositories, + SourceCacheContext sourceCacheContext, + ILogger logger) { - private readonly IEnumerable _packages; - private readonly List _sourceRepositories; - private readonly ILogger _logger; - private readonly SourceCacheContext _sourceCacheContext; - private readonly PackageVulnerabilitySeverity _minSeverity; - - public AuditUtility( - PackageVulnerabilitySeverity minSeverity, - IEnumerable packages, - List sourceRepositories, - SourceCacheContext sourceCacheContext, - ILogger logger) - { - _minSeverity = minSeverity; - _packages = packages; - _sourceRepositories = sourceRepositories; - _sourceCacheContext = sourceCacheContext; - _logger = logger; - } + private readonly List _sourceRepositories = sourceRepositories; + private readonly ILogger _logger = logger; + private readonly SourceCacheContext _sourceCacheContext = sourceCacheContext; - public async Task CheckPackageVulnerabilitiesAsync(CancellationToken cancellationToken) + public async Task CheckPackageVulnerabilitiesAsync(IEnumerable packages, Dictionary restoreAuditProperties, CancellationToken cancellationToken) { - GetVulnerabilityInfoResult? allVulnerabilityData = await GetAllVulnerabilityDataAsync(_sourceRepositories, _sourceCacheContext, _logger, cancellationToken); + if (packages == null) throw new ArgumentNullException(nameof(packages)); + if (restoreAuditProperties == null) throw new ArgumentNullException(nameof(restoreAuditProperties)); + + // Before fetching vulnerability data, check if any projects are enabled for audit + // If there are no settings, then run the audit for all packages + bool anyProjectsEnabledForAudit = restoreAuditProperties.Count == 0; + var auditSettings = new Dictionary(restoreAuditProperties.Count); + foreach (var (projectPath, restoreAuditProperty) in restoreAuditProperties) + { + _ = restoreAuditProperty.TryParseEnableAudit(out bool isAuditEnabled); + _ = restoreAuditProperty.TryParseAuditLevel(out PackageVulnerabilitySeverity minimumAuditSeverity); + auditSettings.Add(projectPath, (isAuditEnabled, minimumAuditSeverity)); + anyProjectsEnabledForAudit |= isAuditEnabled; + } + + if (!anyProjectsEnabledForAudit) + { + return new AuditCheckResult(Array.Empty()) + { + IsAuditEnabled = false, + }; + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + (int sourceWithVulnerabilityCount, GetVulnerabilityInfoResult? allVulnerabilityData) = await GetAllVulnerabilityDataAsync(_sourceRepositories, _sourceCacheContext, _logger, cancellationToken); + stopwatch.Stop(); + double downloadDurationInSeconds = stopwatch.Elapsed.TotalSeconds; if (allVulnerabilityData?.Exceptions is not null) { @@ -55,18 +71,44 @@ public async Task CheckPackageVulnerabilitiesAsync(CancellationToken cancellatio if (allVulnerabilityData is null || !IsAnyVulnerabilityDataFound(allVulnerabilityData.KnownVulnerabilities)) { - return; + return new AuditCheckResult(Array.Empty()) + { + DownloadDurationInSeconds = downloadDurationInSeconds, + SourcesWithVulnerabilities = sourceWithVulnerabilityCount, + }; } - Dictionary? packagesWithKnownVulnerabilities = - FindPackagesWithKnownVulnerabilities(allVulnerabilityData.KnownVulnerabilities!, - _packages, - _minSeverity); - if (packagesWithKnownVulnerabilities is not null) + stopwatch.Restart(); + Dictionary? packagesWithKnownVulnerabilities = FindPackagesWithKnownVulnerabilities(allVulnerabilityData.KnownVulnerabilities!, packages); + int Sev0Matches = 0, Sev1Matches = 0, Sev2Matches = 0, Sev3Matches = 0, InvalidSevMatches = 0; + + List packagesWithReportedAdvisories = new(packagesWithKnownVulnerabilities?.Count ?? 0); + + IReadOnlyList warnings = packagesWithKnownVulnerabilities is not null ? + CreateWarnings(packagesWithKnownVulnerabilities, auditSettings, ref Sev0Matches, ref Sev1Matches, ref Sev2Matches, ref Sev3Matches, ref InvalidSevMatches, ref packagesWithReportedAdvisories) : + Array.Empty(); + + foreach (var warning in warnings) { - CreateWarningsForPackagesWithVulnerabilities(packagesWithKnownVulnerabilities, _logger); + _logger.Log(warning); } + stopwatch.Stop(); + double checkPackagesDurationInSeconds = stopwatch.Elapsed.TotalSeconds; + + return new AuditCheckResult(warnings) + { + Severity0VulnerabilitiesFound = Sev0Matches, + Severity1VulnerabilitiesFound = Sev1Matches, + Severity2VulnerabilitiesFound = Sev2Matches, + Severity3VulnerabilitiesFound = Sev3Matches, + InvalidSeverityVulnerabilitiesFound = InvalidSevMatches, + Packages = packagesWithReportedAdvisories, + DownloadDurationInSeconds = downloadDurationInSeconds, + CheckPackagesDurationInSeconds = checkPackagesDurationInSeconds, + SourcesWithVulnerabilities = sourceWithVulnerabilityCount, + }; + static bool IsAnyVulnerabilityDataFound(IReadOnlyList>>? knownVulnerabilities) { if (knownVulnerabilities is null || knownVulnerabilities.Count == 0) @@ -82,8 +124,9 @@ static bool IsAnyVulnerabilityDataFound(IReadOnlyList GetAllVulnerabilityDataAsync(List sourceRepositories, SourceCacheContext sourceCacheContext, ILogger logger, CancellationToken cancellationToken) + internal static async Task<(int, GetVulnerabilityInfoResult?)> GetAllVulnerabilityDataAsync(List sourceRepositories, SourceCacheContext sourceCacheContext, ILogger logger, CancellationToken cancellationToken) { + int SourcesWithVulnerabilityData = 0; List>? results = new(sourceRepositories.Count); foreach (SourceRepository source in sourceRepositories) @@ -110,20 +153,15 @@ static bool IsAnyVulnerabilityDataFound(IReadOnlyList GetVulnerabilityInfoAsync(SourceRepository source, SourceCacheContext cacheContext, ILogger logger) { @@ -156,10 +194,20 @@ static bool IsAnyVulnerabilityDataFound(IReadOnlyList packagesWithKnownVulnerabilities, ILogger logger) + internal static List CreateWarnings(Dictionary packagesWithKnownVulnerabilities, + Dictionary auditSettings, + ref int Sev0Matches, + ref int Sev1Matches, + ref int Sev2Matches, + ref int Sev3Matches, + ref int InvalidSevMatches, + ref List packagesWithReportedAdvisories) { + var warnings = new List(); foreach ((PackageIdentity package, PackageAuditInfo auditInfo) in packagesWithKnownVulnerabilities.OrderBy(p => p.Key.Id)) { + bool isVulnerabilityReported = false; + foreach (PackageVulnerabilityInfo vulnerability in auditInfo.Vulnerabilities) { (var severityLabel, NuGetLogCode logCode) = GetSeverityLabelAndCode(vulnerability.Severity); @@ -168,22 +216,60 @@ internal static void CreateWarningsForPackagesWithVulnerabilities(Dictionary= (int)auditSetting.MinimumSeverity) + { + isVulnerabilityReported = true; + if (!counted) + { + switch (vulnerability.Severity) + { + case PackageVulnerabilitySeverity.Low: + Sev0Matches++; + break; + case PackageVulnerabilitySeverity.Moderate: + Sev1Matches++; + break; + case PackageVulnerabilitySeverity.High: + Sev2Matches++; + break; + case PackageVulnerabilitySeverity.Critical: + Sev3Matches++; + break; + default: + InvalidSevMatches++; + break; + } + } + counted = true; + + var restoreLogMessage = LogMessage.CreateWarning(logCode, message); + restoreLogMessage.ProjectPath = projectPath; + warnings.Add(restoreLogMessage); + } + } + } + if (isVulnerabilityReported) + { + packagesWithReportedAdvisories.Add(package); } } + return warnings; } internal static Dictionary? FindPackagesWithKnownVulnerabilities( IReadOnlyList>> knownVulnerabilities, - IEnumerable packages, PackageVulnerabilitySeverity minSeverity) + IEnumerable packages) { Dictionary? result = null; - foreach (PackageRestoreData packageRestoreData in packages) + foreach (PackageRestoreData packageRestoreData in packages.NoAllocEnumerate()) { PackageIdentity packageIdentity = packageRestoreData.PackageReference.PackageIdentity; List? knownVulnerabilitiesForPackage = GetKnownVulnerabilities(packageIdentity.Id, packageIdentity.Version, knownVulnerabilities); @@ -192,16 +278,11 @@ internal static void CreateWarningsForPackagesWithVulnerabilities(Dictionary Projects { get; } + public List Vulnerabilities { get; } - public PackageAuditInfo(PackageIdentity identity) + public PackageAuditInfo(PackageIdentity identity, IList projects) { Identity = identity; Vulnerabilities = new(); + Projects = projects; } } } diff --git a/src/NuGet.Core/NuGet.PackageManagement/GlobalSuppressions.cs b/src/NuGet.Core/NuGet.PackageManagement/GlobalSuppressions.cs index 7a778aa94f6..bdbfc6a6ab7 100644 --- a/src/NuGet.Core/NuGet.PackageManagement/GlobalSuppressions.cs +++ b/src/NuGet.Core/NuGet.PackageManagement/GlobalSuppressions.cs @@ -43,8 +43,6 @@ [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'bool PackageReferenceComparer.Equals(PackageReference x, PackageReference y)', validate parameter 'y' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PackageReferenceComparer.Equals(NuGet.Packaging.PackageReference,NuGet.Packaging.PackageReference)~System.Boolean")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'int PackageReferenceComparer.GetHashCode(PackageReference obj)', validate parameter 'obj' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PackageReferenceComparer.GetHashCode(NuGet.Packaging.PackageReference)~System.Int32")] [assembly: SuppressMessage("Build", "CA1031:Modify 'GetPackagesReferencesDictionaryAsync' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PackageRestoreManager.GetPackagesReferencesDictionaryAsync(System.Threading.CancellationToken)~System.Threading.Tasks.Task{System.Collections.Generic.Dictionary{NuGet.Packaging.PackageReference,System.Collections.Generic.List{System.String}}}")] -[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'Task PackageRestoreManager.RestoreMissingPackagesAsync(string solutionDirectory, IEnumerable packages, INuGetProjectContext nuGetProjectContext, PackageDownloadContext downloadContext, ILogger logger, CancellationToken token)', validate parameter 'nuGetProjectContext' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PackageRestoreManager.RestoreMissingPackagesAsync(System.String,System.Collections.Generic.IEnumerable{NuGet.PackageManagement.PackageRestoreData},NuGet.ProjectManagement.INuGetProjectContext,NuGet.Protocol.Core.Types.PackageDownloadContext,NuGet.Common.ILogger,System.Threading.CancellationToken)~System.Threading.Tasks.Task{NuGet.PackageManagement.PackageRestoreResult}")] -[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'Task PackageRestoreManager.RestoreMissingPackagesAsync(string solutionDirectory, IEnumerable packages, INuGetProjectContext nuGetProjectContext, PackageDownloadContext downloadContext, CancellationToken token)', validate parameter 'nuGetProjectContext' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PackageRestoreManager.RestoreMissingPackagesAsync(System.String,System.Collections.Generic.IEnumerable{NuGet.PackageManagement.PackageRestoreData},NuGet.ProjectManagement.INuGetProjectContext,NuGet.Protocol.Core.Types.PackageDownloadContext,System.Threading.CancellationToken)~System.Threading.Tasks.Task{NuGet.PackageManagement.PackageRestoreResult}")] [assembly: SuppressMessage("Build", "CA1031:Modify 'RestorePackageAsync' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PackageRestoreManager.RestorePackageAsync(NuGet.Packaging.PackageReference,NuGet.PackageManagement.PackageRestoreContext,NuGet.ProjectManagement.INuGetProjectContext,NuGet.Protocol.Core.Types.PackageDownloadContext)~System.Threading.Tasks.Task{NuGet.PackageManagement.PackageRestoreManager.AttemptedPackage}")] [assembly: SuppressMessage("Build", "CA1822:Member GetNupkgMetadataPath does not access instance data and can be marked as static (Shared in VisualBasic)", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PackagesConfigContentHashProvider.GetNupkgMetadataPath(System.String)~System.String")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void ProjectContextLogger.Log(ILogMessage message)', validate parameter 'message' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.ProjectContextLogger.Log(NuGet.Common.ILogMessage)")] @@ -55,7 +53,6 @@ [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'IEnumerable PrunePackageTree.PruneDisallowedVersions(IEnumerable packages, IEnumerable packageReferences)', validate parameter 'packageReferences' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PrunePackageTree.PruneDisallowedVersions(System.Collections.Generic.IEnumerable{NuGet.Protocol.Core.Types.SourcePackageDependencyInfo},System.Collections.Generic.IEnumerable{NuGet.Packaging.PackageReference})~System.Collections.Generic.IEnumerable{NuGet.Protocol.Core.Types.SourcePackageDependencyInfo}")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'IEnumerable PrunePackageTree.PruneDowngrades(IEnumerable packages, IEnumerable packageReferences)', validate parameter 'packageReferences' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PrunePackageTree.PruneDowngrades(System.Collections.Generic.IEnumerable{NuGet.Protocol.Core.Types.SourcePackageDependencyInfo},System.Collections.Generic.IEnumerable{NuGet.Packaging.PackageReference})~System.Collections.Generic.IEnumerable{NuGet.Protocol.Core.Types.SourcePackageDependencyInfo}")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'IEnumerable PrunePackageTree.RemoveDisallowedVersions(IEnumerable packages, PackageReference packageReference)', validate parameter 'packageReference' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.PrunePackageTree.RemoveDisallowedVersions(System.Collections.Generic.IEnumerable{NuGet.Protocol.Core.Types.SourcePackageDependencyInfo},NuGet.Packaging.PackageReference)~System.Collections.Generic.IEnumerable{NuGet.Protocol.Core.Types.SourcePackageDependencyInfo}")] -[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'Task> ResolverGather.GatherAsync(GatherContext context, CancellationToken token)', validate parameter 'context' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.ResolverGather.GatherAsync(NuGet.PackageManagement.GatherContext,System.Threading.CancellationToken)~System.Threading.Tasks.Task{System.Collections.Generic.HashSet{NuGet.Protocol.Core.Types.SourcePackageDependencyInfo}}")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'bool SourceRepositoryComparer.Equals(SourceRepository x, SourceRepository y)', validate parameter 'x' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.SourceRepositoryComparer.Equals(NuGet.Protocol.Core.Types.SourceRepository,NuGet.Protocol.Core.Types.SourceRepository)~System.Boolean")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'int SourceRepositoryComparer.GetHashCode(SourceRepository obj)', validate parameter 'obj' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.SourceRepositoryComparer.GetHashCode(NuGet.Protocol.Core.Types.SourceRepository)~System.Int32")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'IDictionary> UninstallResolver.GetPackageDependents(IEnumerable dependencyInfoEnumerable, IEnumerable installedPackages, out IDictionary> dependenciesDict)', validate parameter 'dependencyInfoEnumerable' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.UninstallResolver.GetPackageDependents(System.Collections.Generic.IEnumerable{NuGet.Packaging.Core.PackageDependencyInfo},System.Collections.Generic.IEnumerable{NuGet.Packaging.Core.PackageIdentity},System.Collections.Generic.IDictionary{NuGet.Packaging.Core.PackageIdentity,System.Collections.Generic.HashSet{NuGet.Packaging.Core.PackageIdentity}}@)~System.Collections.Generic.IDictionary{NuGet.Packaging.Core.PackageIdentity,System.Collections.Generic.HashSet{NuGet.Packaging.Core.PackageIdentity}}")] @@ -122,3 +119,4 @@ [assembly: SuppressMessage("Build", "CA2237:Add [Serializable] to PackageReferenceRollbackException as this type implements ISerializable", Justification = "", Scope = "type", Target = "~T:NuGet.PackageManagement.PackageReferenceRollbackException")] [assembly: SuppressMessage("Build", "CA1067:Type NuGet.ProjectManagement.FileTransformExtensions should override Equals because it implements IEquatable", Justification = "", Scope = "type", Target = "~T:NuGet.ProjectManagement.FileTransformExtensions")] [assembly: SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "", Scope = "member", Target = "~M:NuGet.PackageManagement.NuGetProjectAction.#ctor(NuGet.Packaging.Core.PackageIdentity,NuGet.PackageManagement.NuGetProjectActionType,NuGet.ProjectManagement.NuGetProject,NuGet.Protocol.Core.Types.SourceRepository)")] +[assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Packages are normalized to lowercase", Scope = "member", Target = "~M:NuGet.PackageManagement.AuditCheckResult.AddMetricsToTelemetry(NuGet.Common.TelemetryEvent)")] diff --git a/src/NuGet.Core/NuGet.PackageManagement/IDE/IPackageRestoreManager.cs b/src/NuGet.Core/NuGet.PackageManagement/IDE/IPackageRestoreManager.cs index 00790ae8218..e6fe1939b16 100644 --- a/src/NuGet.Core/NuGet.PackageManagement/IDE/IPackageRestoreManager.cs +++ b/src/NuGet.Core/NuGet.PackageManagement/IDE/IPackageRestoreManager.cs @@ -85,6 +85,7 @@ Task RestoreMissingPackagesInSolutionAsync(string solution /// are missing /// /// Returns true if atleast one package was restored. + [Obsolete("This method is deprecated to reduce complexity, please use one of the other RestoreMissingPackagesAsync methods.")] Task RestoreMissingPackagesInSolutionAsync(string solutionDirectory, INuGetProjectContext nuGetProjectContext, CancellationToken token); @@ -128,6 +129,7 @@ Task RestoreMissingPackagesAsync(string solutionDirectory, /// Returns true if at least one package is restored. Raised package restored failed event with the /// list of project names. /// + [Obsolete("This method is deprecated to reduce complexity, please use one of the other RestoreMissingPackagesAsync methods.")] Task RestoreMissingPackagesAsync(string solutionDirectory, IEnumerable packages, INuGetProjectContext nuGetProjectContext, diff --git a/src/NuGet.Core/NuGet.PackageManagement/IDE/PackageRestoreContext.cs b/src/NuGet.Core/NuGet.PackageManagement/IDE/PackageRestoreContext.cs index a174fc2a188..709cc1a5eb0 100644 --- a/src/NuGet.Core/NuGet.PackageManagement/IDE/PackageRestoreContext.cs +++ b/src/NuGet.Core/NuGet.PackageManagement/IDE/PackageRestoreContext.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading; using NuGet.Common; +using NuGet.ProjectModel; using NuGet.Protocol.Core.Types; namespace NuGet.PackageManagement @@ -19,7 +20,10 @@ public class PackageRestoreContext public IEnumerable SourceRepositories { get; } public int MaxNumberOfParallelTasks { get; } public ILogger Logger { get; } + public bool EnableNuGetAudit { get; } + public Dictionary RestoreAuditProperties { get; } + private static Dictionary EmptyDictionary = new Dictionary(); public PackageRestoreContext(NuGetPackageManager nuGetPackageManager, IEnumerable packages, CancellationToken token, @@ -27,6 +31,19 @@ public PackageRestoreContext(NuGetPackageManager nuGetPackageManager, EventHandler packageRestoreFailedEvent, IEnumerable sourceRepositories, int maxNumberOfParallelTasks, + ILogger logger) : this(nuGetPackageManager, packages, token, packageRestoredEvent, packageRestoreFailedEvent, sourceRepositories, maxNumberOfParallelTasks, false, EmptyDictionary, logger) + { + } + + public PackageRestoreContext(NuGetPackageManager nuGetPackageManager, + IEnumerable packages, + CancellationToken token, + EventHandler packageRestoredEvent, + EventHandler packageRestoreFailedEvent, + IEnumerable sourceRepositories, + int maxNumberOfParallelTasks, + bool enableNuGetAudit, + Dictionary restoreAuditProperties, ILogger logger) { if (maxNumberOfParallelTasks <= 0) @@ -42,6 +59,8 @@ public PackageRestoreContext(NuGetPackageManager nuGetPackageManager, PackageRestoreFailedEvent = packageRestoreFailedEvent; SourceRepositories = sourceRepositories; MaxNumberOfParallelTasks = maxNumberOfParallelTasks; + EnableNuGetAudit = enableNuGetAudit; + RestoreAuditProperties = restoreAuditProperties ?? throw new ArgumentNullException(nameof(restoreAuditProperties)); } } } diff --git a/src/NuGet.Core/NuGet.PackageManagement/IDE/PackageRestoreManager.cs b/src/NuGet.Core/NuGet.PackageManagement/IDE/PackageRestoreManager.cs index 914d5fef5e1..dd9bee6a9b1 100644 --- a/src/NuGet.Core/NuGet.PackageManagement/IDE/PackageRestoreManager.cs +++ b/src/NuGet.Core/NuGet.PackageManagement/IDE/PackageRestoreManager.cs @@ -17,7 +17,9 @@ using NuGet.Packaging.Signing; using NuGet.ProjectManagement; using NuGet.ProjectManagement.Projects; +using NuGet.ProjectModel; using NuGet.Protocol.Core.Types; +using NuGet.Shared; namespace NuGet.PackageManagement { @@ -180,6 +182,7 @@ private async Task>> GetPackagesRefere /// Restores missing packages for the entire solution /// /// + [Obsolete("This method is deprecated to reduce complexity, please use one of the other RestoreMissingPackagesAsync methods.")] public virtual async Task RestoreMissingPackagesInSolutionAsync( string solutionDirectory, INuGetProjectContext nuGetProjectContext, @@ -224,6 +227,8 @@ public virtual async Task RestoreMissingPackagesInSolution ILogger logger, CancellationToken token) { + if (nuGetProjectContext == null) throw new ArgumentNullException(nameof(nuGetProjectContext)); + var packageReferencesDictionary = await GetPackagesReferencesDictionaryAsync(token); // When this method is called, the step to compute if a package is missing is implicit. Assume it is true @@ -253,16 +258,15 @@ public virtual async Task RestoreMissingPackagesInSolution } } + [Obsolete("This method is deprecated to reduce complexity, please use one of the other RestoreMissingPackagesAsync methods.")] public virtual Task RestoreMissingPackagesAsync(string solutionDirectory, IEnumerable packages, INuGetProjectContext nuGetProjectContext, PackageDownloadContext downloadContext, CancellationToken token) { - if (packages == null) - { - throw new ArgumentNullException(nameof(packages)); - } + if (packages == null) throw new ArgumentNullException(nameof(packages)); + if (nuGetProjectContext == null) throw new ArgumentNullException(nameof(nuGetProjectContext)); var nuGetPackageManager = GetNuGetPackageManager(solutionDirectory); @@ -274,6 +278,8 @@ public virtual Task RestoreMissingPackagesAsync(string sol PackageRestoreFailedEvent, sourceRepositories: SourceRepositoryProvider.GetRepositories(), maxNumberOfParallelTasks: PackageManagementConstants.DefaultMaxDegreeOfParallelism, + enableNuGetAudit: true, + restoreAuditProperties: new Dictionary(), logger: NullLogger.Instance); if (nuGetProjectContext.PackageExtractionContext == null) @@ -361,9 +367,12 @@ public static async Task RestoreMissingPackagesAsync( ActivityCorrelationId.StartNew(); + List sourceRepositories = packageRestoreContext.SourceRepositories.AsList(); + var missingPackages = packageRestoreContext.Packages.Where(p => p.IsMissing).ToList(); if (!missingPackages.Any()) { + await RunNuGetAudit(packageRestoreContext, sourceRepositories); return new PackageRestoreResult(true, Enumerable.Empty()); } @@ -376,7 +385,7 @@ public static async Task RestoreMissingPackagesAsync( packageRestoreContext.Token.ThrowIfCancellationRequested(); - foreach (SourceRepository enabledSource in packageRestoreContext.SourceRepositories) + foreach (SourceRepository enabledSource in sourceRepositories) { PackageSource source = enabledSource.PackageSource; if (source.IsHttp && !source.IsHttps && !source.AllowInsecureConnections) @@ -398,11 +407,26 @@ await ThrottledCopySatelliteFilesAsync( packageRestoreContext, nuGetProjectContext); + await RunNuGetAudit(packageRestoreContext, sourceRepositories); + return new PackageRestoreResult( attemptedPackages.All(p => p.Restored), attemptedPackages.Select(p => p.Package.PackageIdentity).ToList()); } + private static async Task RunNuGetAudit(PackageRestoreContext packageRestoreContext, List sourceRepositories) + { + if (packageRestoreContext.EnableNuGetAudit) + { + using SourceCacheContext sourceCacheContext = new(); + var auditUtility = new AuditChecker( + sourceRepositories, + sourceCacheContext, + packageRestoreContext.Logger); + await auditUtility.CheckPackageVulnerabilitiesAsync(packageRestoreContext.Packages, packageRestoreContext.RestoreAuditProperties, packageRestoreContext.Token); + } + } + /// /// ThrottledPackageRestoreAsync method throttles the number of tasks created to perform package restore in /// parallel diff --git a/src/NuGet.Core/NuGet.PackageManagement/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.PackageManagement/PublicAPI.Unshipped.txt index 7dc5c58110b..76501cb9939 100644 --- a/src/NuGet.Core/NuGet.PackageManagement/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.PackageManagement/PublicAPI.Unshipped.txt @@ -1 +1,10 @@ #nullable enable +NuGet.PackageManagement.AuditChecker +NuGet.PackageManagement.AuditChecker.AuditChecker(System.Collections.Generic.List! sourceRepositories, NuGet.Protocol.Core.Types.SourceCacheContext! sourceCacheContext, NuGet.Common.ILogger! logger) -> void +NuGet.PackageManagement.AuditChecker.CheckPackageVulnerabilitiesAsync(System.Collections.Generic.IEnumerable! packages, System.Collections.Generic.Dictionary! restoreAuditProperties, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +NuGet.PackageManagement.AuditCheckResult +NuGet.PackageManagement.AuditCheckResult.AddMetricsToTelemetry(NuGet.Common.TelemetryEvent! telemetryEvent) -> void +NuGet.PackageManagement.AuditCheckResult.Warnings.get -> System.Collections.Generic.IReadOnlyList! +NuGet.PackageManagement.PackageRestoreContext.EnableNuGetAudit.get -> bool +~NuGet.PackageManagement.PackageRestoreContext.PackageRestoreContext(NuGet.PackageManagement.NuGetPackageManager nuGetPackageManager, System.Collections.Generic.IEnumerable packages, System.Threading.CancellationToken token, System.EventHandler packageRestoredEvent, System.EventHandler packageRestoreFailedEvent, System.Collections.Generic.IEnumerable sourceRepositories, int maxNumberOfParallelTasks, bool enableNuGetAudit, System.Collections.Generic.Dictionary restoreAuditProperties, NuGet.Common.ILogger logger) -> void +~NuGet.PackageManagement.PackageRestoreContext.RestoreAuditProperties.get -> System.Collections.Generic.Dictionary diff --git a/src/NuGet.Core/NuGet.PackageManagement/Resolution/ResolverGather.cs b/src/NuGet.Core/NuGet.PackageManagement/Resolution/ResolverGather.cs index 4581b41f8ed..6b075cc2734 100644 --- a/src/NuGet.Core/NuGet.PackageManagement/Resolution/ResolverGather.cs +++ b/src/NuGet.Core/NuGet.PackageManagement/Resolution/ResolverGather.cs @@ -75,6 +75,7 @@ public static async Task> GatherAsync( GatherContext context, CancellationToken token) { + if (context == null) throw new ArgumentNullException(nameof(context)); var engine = new ResolverGather(context); return await engine.GatherAsync(token); } diff --git a/src/NuGet.Core/NuGet.ProjectModel/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.ProjectModel/PublicAPI.Unshipped.txt index c1bae6a05a4..f6d93463dd9 100644 --- a/src/NuGet.Core/NuGet.ProjectModel/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.ProjectModel/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ #nullable enable +NuGet.ProjectModel.RestoreAuditProperties.TryParseAuditLevel(out NuGet.Protocol.PackageVulnerabilitySeverity result) -> bool +NuGet.ProjectModel.RestoreAuditProperties.TryParseEnableAudit(out bool result) -> bool ~NuGet.ProjectModel.HashObjectWriter.WriteNonEmptyNameArray(string name, System.Collections.Generic.IEnumerable values) -> void diff --git a/src/NuGet.Core/NuGet.ProjectModel/RestoreAuditProperties.cs b/src/NuGet.Core/NuGet.ProjectModel/RestoreAuditProperties.cs index c2b8d1728eb..4e4862f2158 100644 --- a/src/NuGet.Core/NuGet.ProjectModel/RestoreAuditProperties.cs +++ b/src/NuGet.Core/NuGet.ProjectModel/RestoreAuditProperties.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using NuGet.Protocol; using NuGet.Shared; namespace NuGet.ProjectModel @@ -27,6 +28,62 @@ public class RestoreAuditProperties : IEquatable /// direct, all public string? AuditMode { get; set; } + // Enum parsing and ToString are a magnitude of times slower than a naive implementation. + public bool TryParseEnableAudit(out bool result) + { + // Earlier versions allowed "enable" and "default" to opt-in + if (string.IsNullOrEmpty(EnableAudit) + || string.Equals(EnableAudit, bool.TrueString, StringComparison.OrdinalIgnoreCase) + || string.Equals(EnableAudit, "enable", StringComparison.OrdinalIgnoreCase) + || string.Equals(EnableAudit, "default", StringComparison.OrdinalIgnoreCase)) + { + result = true; + return true; + } + if (string.Equals(EnableAudit, bool.FalseString, StringComparison.OrdinalIgnoreCase) + || string.Equals(EnableAudit, "disable", StringComparison.OrdinalIgnoreCase)) + { + result = false; + return true; + } + result = true; + + return false; + } + + public bool TryParseAuditLevel(out PackageVulnerabilitySeverity result) + { + if (AuditLevel == null) + { + result = PackageVulnerabilitySeverity.Low; + return true; + } + + if (string.Equals(AuditLevel, "low", StringComparison.OrdinalIgnoreCase)) + { + result = PackageVulnerabilitySeverity.Low; + return true; + } + if (string.Equals(AuditLevel, "moderate", StringComparison.OrdinalIgnoreCase)) + { + result = PackageVulnerabilitySeverity.Moderate; + return true; + } + if (string.Equals(AuditLevel, "high", StringComparison.OrdinalIgnoreCase)) + { + result = PackageVulnerabilitySeverity.High; + return true; + } + if (string.Equals(AuditLevel, "critical", StringComparison.OrdinalIgnoreCase)) + { + result = PackageVulnerabilitySeverity.Critical; + return true; + } + + result = PackageVulnerabilitySeverity.Unknown; + return false; + } + public bool Equals(RestoreAuditProperties? other) { if (other is null) return false; diff --git a/test/NuGet.Clients.Tests/NuGet.CommandLine.Test/NuGetInstallCommandTest.cs b/test/NuGet.Clients.Tests/NuGet.CommandLine.Test/NuGetInstallCommandTest.cs index 4074d4a3356..ca07531869a 100644 --- a/test/NuGet.Clients.Tests/NuGet.CommandLine.Test/NuGetInstallCommandTest.cs +++ b/test/NuGet.Clients.Tests/NuGet.CommandLine.Test/NuGetInstallCommandTest.cs @@ -2137,6 +2137,42 @@ public async Task Install_WithPackageIdAndHttpSource_Warns() result.AllOutput.Should().Contain("You are running the 'install' operation with an 'HTTP' source"); } + // https://github.com/NuGet/Home/issues/8594" + [SkipMono()] + public async Task InstallCommand_WithASourceThatReportsVulnerabilities_RaisesVulnerabilityWarnings() + { + using var pathContext = new SimpleTestPathContext(); + using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, sourceReportsVulnerabilities: true); + + //Replace the default package source of folder to ServiceIndexUri + var settings = pathContext.Settings; + SimpleTestSettingsContext.RemoveSource(settings.XML, "source"); + var section = SimpleTestSettingsContext.GetOrAddSection(settings.XML, "packageSources"); + SimpleTestSettingsContext.AddEntry(section, "source", mockServer.ServiceIndexUri); + settings.Save(); + + // Arrange + var a1 = new SimpleTestPackageContext("a", "1.0.0"); + var a2 = new SimpleTestPackageContext("a", "2.0.0"); + + SimpleTestPackageContext[] packages = [a1, a2]; + await SimpleTestPackageUtility.CreatePackagesAsync(pathContext.PackageSource, packages); + + mockServer.Start(); + var pathResolver = new PackagePathResolver(pathContext.SolutionRoot); + + // Act + var r1 = RunInstall(pathContext, "a", 0, "-Version", "2.0.0", "-OutputDirectory", pathContext.SolutionRoot); + + mockServer.Stop(); + + // Assert + var a1Nupkg = pathResolver.GetInstalledPackageFilePath(a1.Identity); + + r1.Success.Should().BeTrue(); + File.Exists(a1Nupkg).Should().BeFalse(); + } + public static CommandRunnerResult RunInstall(SimpleTestPathContext pathContext, string input, int expectedExitCode = 0, params string[] additionalArgs) { var nugetexe = Util.GetNuGetExePath(); diff --git a/test/NuGet.Clients.Tests/NuGet.CommandLine.Test/NuGetRestoreCommandTest.cs b/test/NuGet.Clients.Tests/NuGet.CommandLine.Test/NuGetRestoreCommandTest.cs index 8fd0249e601..8a92a4f6725 100644 --- a/test/NuGet.Clients.Tests/NuGet.CommandLine.Test/NuGetRestoreCommandTest.cs +++ b/test/NuGet.Clients.Tests/NuGet.CommandLine.Test/NuGetRestoreCommandTest.cs @@ -20,6 +20,7 @@ using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.ProjectModel; +using NuGet.Protocol; using NuGet.Test.Utility; using NuGet.Versioning; using Test.Utility; @@ -3290,6 +3291,288 @@ public void RestoreCommand_PackageSourceMapping_InternationalSources_SearchMatch } } + [SkipMono()] + public void RestoreCommand_WithPackagesConfig_PackageWithVulnerabilities_RaisesWarnings() + { + // Arrange + var nugetexe = Util.GetNuGetExePath(); + using var pathContext = new SimpleTestPathContext(); + using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, sourceReportsVulnerabilities: true); + + mockServer.Vulnerabilities.Add( + "packageA", + new List<(Uri, PackageVulnerabilitySeverity, VersionRange)> { + (new Uri("https://contoso.com/advisories/12345"), PackageVulnerabilitySeverity.High, VersionRange.Parse("[1.0.0, 2.0.0)")) + }); + pathContext.Settings.RemoveSource("source"); + pathContext.Settings.AddSource("source", mockServer.ServiceIndexUri); + + var workingPath = pathContext.WorkingDirectory; + Util.CreateTestPackage("packageA", "1.1.0", pathContext.PackageSource); + Util.CreateTestPackage("packageB", "2.2.0", pathContext.PackageSource); + Util.CreateFile(workingPath, "packages.config", +@" + + +"); + + string[] args = ["restore", "-PackagesDirectory", "outputDir"]; + mockServer.Start(); + + // Act + var r = CommandRunner.Run( + nugetexe, + workingPath, + string.Join(" ", args)); + + mockServer.Stop(); + + // Assert + r.Success.Should().BeTrue(because: r.AllOutput); + var packageFileA = Path.Combine(workingPath, @"outputDir", "packageA.1.1.0", "packageA.1.1.0.nupkg"); + var packageFileB = Path.Combine(workingPath, @"outputDir", "packageB.2.2.0", "packageB.2.2.0.nupkg"); + File.Exists(packageFileA).Should().BeTrue(); + File.Exists(packageFileB).Should().BeTrue(); + r.AllOutput.Should().Contain($"Package 'packageA' 1.1.0 has a known high severity vulnerability"); + } + + [SkipMono()] + public void RestoreCommand_WithProjectWithPackagesConfig_DefaultSettings_PackageWithVulnerabilities_RaisesWarnings() + { + // Arrange + var nugetexe = Util.GetNuGetExePath(); + using var pathContext = new SimpleTestPathContext(); + using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, sourceReportsVulnerabilities: true); + + mockServer.Vulnerabilities.Add( + "packageA", + new List<(Uri, PackageVulnerabilitySeverity, VersionRange)> { + (new Uri("https://contoso.com/advisories/12345"), PackageVulnerabilitySeverity.High, VersionRange.Parse("[1.0.0, 2.0.0)")) + }); + + pathContext.Settings.RemoveSource("source"); + pathContext.Settings.AddSource("source", mockServer.ServiceIndexUri); + + Util.CreateTestPackage("packageA", "1.1.0", pathContext.PackageSource); + Util.CreateTestPackage("packageB", "2.2.0", pathContext.PackageSource); + + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = new SimpleTestProjectContext( + "a", + ProjectStyle.PackagesConfig, + pathContext.SolutionRoot); + + solution.Projects.Add(projectA); + solution.Create(pathContext.SolutionRoot); + + Util.CreateFile(Path.GetDirectoryName(projectA.ProjectPath), "packages.config", +@" + + +"); + + mockServer.Start(); + + // Act + var r = CommandRunner.Run( + nugetexe, + pathContext.WorkingDirectory, + $"restore {projectA.ProjectPath}"); + + mockServer.Stop(); + + // Assert + r.Success.Should().BeTrue(because: r.AllOutput); + var packageFileA = Path.Combine(pathContext.SolutionRoot, "packages", "packageA.1.1.0", "packageA.1.1.0.nupkg"); + var packageFileB = Path.Combine(pathContext.SolutionRoot, "packages", "packageB.2.2.0", "packageB.2.2.0.nupkg"); + File.Exists(packageFileA).Should().BeTrue(); + File.Exists(packageFileB).Should().BeTrue(); + r.AllOutput.Should().Contain($"Package 'packageA' 1.1.0 has a known high severity vulnerability"); + } + + [SkipMono()] + public void RestoreCommand_WithProjectWithPackagesConfig_WithNuGetAuditFalse_PackageWithVulnerabilities_DoesNotRaiseWarnings() + { + // Arrange + var nugetexe = Util.GetNuGetExePath(); + using var pathContext = new SimpleTestPathContext(); + using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, sourceReportsVulnerabilities: true); + + mockServer.Vulnerabilities.Add( + "packageA", + new List<(Uri, PackageVulnerabilitySeverity, VersionRange)> { + (new Uri("https://contoso.com/advisories/12345"), PackageVulnerabilitySeverity.High, VersionRange.Parse("[1.0.0, 2.0.0)")) + }); + pathContext.Settings.RemoveSource("source"); + pathContext.Settings.AddSource("source", mockServer.ServiceIndexUri); + + Util.CreateTestPackage("packageA", "1.1.0", pathContext.PackageSource); + Util.CreateTestPackage("packageB", "2.2.0", pathContext.PackageSource); + + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = new SimpleTestProjectContext( + "a", + ProjectStyle.PackagesConfig, + pathContext.SolutionRoot); + projectA.Properties.Add("NuGetAudit", "false"); + + solution.Projects.Add(projectA); + solution.Create(pathContext.SolutionRoot); + + Util.CreateFile(Path.GetDirectoryName(projectA.ProjectPath), "packages.config", +@" + + +"); + + mockServer.Start(); + + // Act + var r = CommandRunner.Run( + nugetexe, + pathContext.WorkingDirectory, + $"restore {projectA.ProjectPath}"); + + mockServer.Stop(); + + // Assert + r.Success.Should().BeTrue(because: r.AllOutput); + var packageFileA = Path.Combine(pathContext.SolutionRoot, "packages", "packageA.1.1.0", "packageA.1.1.0.nupkg"); + var packageFileB = Path.Combine(pathContext.SolutionRoot, "packages", "packageB.2.2.0", "packageB.2.2.0.nupkg"); + File.Exists(packageFileA).Should().BeTrue(); + File.Exists(packageFileB).Should().BeTrue(); + r.AllOutput.Should().NotContain($"Package 'packageA' 1.1.0 has a known high severity vulnerability"); + } + + [SkipMono()] + public void RestoreCommand_WithProjectWithPackagesConfig_WithNuGetAuditLevel_PackageWithVulnerabilities_DoesNotRaiseWarnings() + { + // Arrange + var nugetexe = Util.GetNuGetExePath(); + using var pathContext = new SimpleTestPathContext(); + using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, sourceReportsVulnerabilities: true); + + mockServer.Vulnerabilities.Add( + "packageA", + new List<(Uri, PackageVulnerabilitySeverity, VersionRange)> { + (new Uri("https://contoso.com/advisories/12345"), PackageVulnerabilitySeverity.High, VersionRange.Parse("[1.0.0, 2.0.0)")), + (new Uri("https://contoso.com/advisories/12346"), PackageVulnerabilitySeverity.Critical, VersionRange.Parse("[1.0.0, 2.0.0)")) + }); + pathContext.Settings.RemoveSource("source"); + pathContext.Settings.AddSource("source", mockServer.ServiceIndexUri); + + Util.CreateTestPackage("packageA", "1.1.0", pathContext.PackageSource); + Util.CreateTestPackage("packageB", "2.2.0", pathContext.PackageSource); + + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = new SimpleTestProjectContext( + "a", + ProjectStyle.PackagesConfig, + pathContext.SolutionRoot); + projectA.Properties.Add("NuGetAuditLevel", "critical"); + + solution.Projects.Add(projectA); + solution.Create(pathContext.SolutionRoot); + + Util.CreateFile(Path.GetDirectoryName(projectA.ProjectPath), "packages.config", +@" + + +"); + + mockServer.Start(); + + // Act + var r = CommandRunner.Run( + nugetexe, + pathContext.WorkingDirectory, + $"restore {projectA.ProjectPath}"); + + mockServer.Stop(); + + // Assert + r.Success.Should().BeTrue(because: r.AllOutput); + var packageFileA = Path.Combine(pathContext.SolutionRoot, "packages", "packageA.1.1.0", "packageA.1.1.0.nupkg"); + var packageFileB = Path.Combine(pathContext.SolutionRoot, "packages", "packageB.2.2.0", "packageB.2.2.0.nupkg"); + File.Exists(packageFileA).Should().BeTrue(); + File.Exists(packageFileB).Should().BeTrue(); + r.AllOutput.Should().Contain($"Package 'packageA' 1.1.0 has a known critical severity vulnerability"); + r.AllOutput.Should().NotContain($"Package 'packageA' 1.1.0 has a known high severity vulnerability"); + } + + [SkipMono()] + public void RestoreCommand_WithSolutionFile_PackageWithVulnerabilities_RaisesAppropriateWarnings() + { + // Arrange + var nugetexe = Util.GetNuGetExePath(); + using var pathContext = new SimpleTestPathContext(); + using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, sourceReportsVulnerabilities: true); + + mockServer.Vulnerabilities.Add( + "packageA", + new List<(Uri, PackageVulnerabilitySeverity, VersionRange)> { + (new Uri("https://contoso.com/advisories/12345"), PackageVulnerabilitySeverity.High, VersionRange.Parse("[1.0.0, 2.0.0)")), + (new Uri("https://contoso.com/advisories/12346"), PackageVulnerabilitySeverity.Critical, VersionRange.Parse("[1.2.0, 2.0.0)")) + }); + pathContext.Settings.RemoveSource("source"); + pathContext.Settings.AddSource("source", mockServer.ServiceIndexUri); + + Util.CreateTestPackage("packageA", "1.1.0", pathContext.PackageSource); + Util.CreateTestPackage("packageA", "1.2.0", pathContext.PackageSource); + Util.CreateTestPackage("packageB", "2.2.0", pathContext.PackageSource); + + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = new SimpleTestProjectContext( + "a", + ProjectStyle.PackagesConfig, + pathContext.SolutionRoot); + projectA.Properties.Add("NuGetAuditLevel", "critical"); + + var projectB = new SimpleTestProjectContext( + "B", + ProjectStyle.PackagesConfig, + pathContext.SolutionRoot); + projectB.Properties.Add("NuGetAuditLevel", "high"); + + solution.Projects.Add(projectA); + solution.Projects.Add(projectB); + solution.Create(pathContext.SolutionRoot); + + Util.CreateFile(Path.GetDirectoryName(projectA.ProjectPath), "packages.config", +@" + + +"); + + Util.CreateFile(Path.GetDirectoryName(projectB.ProjectPath), "packages.config", +@" + + +"); + + mockServer.Start(); + + // Act + var r = CommandRunner.Run( + nugetexe, + pathContext.WorkingDirectory, + $"restore {solution.SolutionPath}"); + + mockServer.Stop(); + + // Assert + r.Success.Should().BeTrue(because: r.AllOutput); + var packageFileA = Path.Combine(pathContext.SolutionRoot, "packages", "packageA.1.1.0", "packageA.1.1.0.nupkg"); + var packageFileA120 = Path.Combine(pathContext.SolutionRoot, "packages", "packageA.1.2.0", "packageA.1.2.0.nupkg"); + var packageFileB = Path.Combine(pathContext.SolutionRoot, "packages", "packageB.2.2.0", "packageB.2.2.0.nupkg"); + File.Exists(packageFileA).Should().BeTrue(); + File.Exists(packageFileA120).Should().BeTrue(); + File.Exists(packageFileB).Should().BeTrue(); + r.AllOutput.Should().Contain($"Package 'packageA' 1.2.0 has a known critical severity vulnerability"); + r.AllOutput.Should().Contain($"Package 'packageA' 1.2.0 has a known high severity vulnerability"); + r.AllOutput.Should().NotContain($"Package 'packageA' 1.1.0 has a known high severity vulnerability"); + } + private static byte[] GetResource(string name) { return ResourceTestUtility.GetResourceBytes( diff --git a/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreCommandTests/Utility/AuditUtilityTests.cs b/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreCommandTests/Utility/AuditUtilityTests.cs index 846e6e0bf68..94189829aef 100644 --- a/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreCommandTests/Utility/AuditUtilityTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreCommandTests/Utility/AuditUtilityTests.cs @@ -47,9 +47,12 @@ public void ParseEnableValue_WithValue_ReturnsExpected(string? input, bool expec // Arrange string projectPath = "my.csproj"; TestLogger logger = new TestLogger(); - + RestoreAuditProperties restoreAuditProperties = new() + { + EnableAudit = input + }; // Act - bool actual = AuditUtility.ParseEnableValue(input, projectPath, logger); + bool actual = AuditUtility.ParseEnableValue(restoreAuditProperties, projectPath, logger); // Assert actual.Should().Be(expected); @@ -483,7 +486,14 @@ public VulnerabilityProviderTestContext WithVulnerabilityProvider() public async Task CheckPackageVulnerabilitiesAsync(CancellationToken cancellationToken) { - bool enabled = AuditUtility.ParseEnableValue(Enabled, ProjectFullPath, Log); + RestoreAuditProperties restoreAuditProperties = new() + { + EnableAudit = Enabled, + AuditLevel = Level, + AuditMode = Mode, + }; + + bool enabled = AuditUtility.ParseEnableValue(restoreAuditProperties, ProjectFullPath, Log); if (!enabled) { throw new InvalidOperationException($"{nameof(Enabled)} must have a value that does not disable NuGetAudit."); @@ -494,12 +504,6 @@ public async Task CheckPackageVulnerabilitiesAsync(CancellationTok throw new InvalidOperationException($"{nameof(WithRestoreTarget)} must be called once"); } - RestoreAuditProperties restoreAuditProperties = new() - { - EnableAudit = Enabled, - AuditLevel = Level, - AuditMode = Mode, - }; var graphs = await CreateGraphsAsync(); diff --git a/test/NuGet.Core.Tests/NuGet.PackageManagement.Test/AuditUtilityTests.cs b/test/NuGet.Core.Tests/NuGet.PackageManagement.Test/AuditCheckerTests.cs similarity index 51% rename from test/NuGet.Core.Tests/NuGet.PackageManagement.Test/AuditUtilityTests.cs rename to test/NuGet.Core.Tests/NuGet.PackageManagement.Test/AuditCheckerTests.cs index 1ce0ad63b85..35d95a64ccb 100644 --- a/test/NuGet.Core.Tests/NuGet.PackageManagement.Test/AuditUtilityTests.cs +++ b/test/NuGet.Core.Tests/NuGet.PackageManagement.Test/AuditCheckerTests.cs @@ -14,6 +14,7 @@ using NuGet.Configuration; using NuGet.Packaging; using NuGet.Packaging.Core; +using NuGet.ProjectModel; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Protocol.Model; @@ -21,11 +22,10 @@ using NuGet.Versioning; using Xunit; using static NuGet.Frameworks.FrameworkConstants; -using static NuGet.PackageManagement.AuditUtility; namespace NuGet.PackageManagement.Test { - public class AuditUtilityTests + public class AuditCheckerTests { [Fact] public void GetKnownVulnerability_WithPackageNotVulnerable_ReturnsNull() @@ -45,7 +45,7 @@ public void GetKnownVulnerability_WithPackageNotVulnerable_ReturnsNull() } }; - AuditUtility.GetKnownVulnerabilities("packageId", new NuGetVersion(1, 0, 0), knownVulnerabilities).Should().BeNull(); + AuditChecker.GetKnownVulnerabilities("packageId", new NuGetVersion(1, 0, 0), knownVulnerabilities).Should().BeNull(); } [Fact] @@ -66,7 +66,7 @@ public void GetKnownVulnerability_WithPackageIdVulnerableButPackageVersionNotInR } }; - AuditUtility.GetKnownVulnerabilities("packageId", new NuGetVersion(2, 0, 0), knownVulnerabilities).Should().BeNull(); + AuditChecker.GetKnownVulnerabilities("packageId", new NuGetVersion(2, 0, 0), knownVulnerabilities).Should().BeNull(); } [Fact] @@ -96,7 +96,7 @@ public void GetKnownVulnerability_WithPackageVulnerable_ReturnsOnlyAppropriateVu } }; - List? vulnerabilities = AuditUtility.GetKnownVulnerabilities("packageId", new NuGetVersion(2, 0, 0), knownVulnerabilities); + List? vulnerabilities = AuditChecker.GetKnownVulnerabilities("packageId", new NuGetVersion(2, 0, 0), knownVulnerabilities); vulnerabilities!.Should().HaveCount(2); var moderateVulnerability = vulnerabilities![0]; @@ -148,7 +148,7 @@ public void GetKnownVulnerability_WithPackageVulnerableFromMultipleSources_Retur } }; - List? vulnerabilities = AuditUtility.GetKnownVulnerabilities("packageId", new NuGetVersion(2, 0, 0), knownVulnerabilities); + List? vulnerabilities = AuditChecker.GetKnownVulnerabilities("packageId", new NuGetVersion(2, 0, 0), knownVulnerabilities); vulnerabilities!.Should().HaveCount(2); var moderateVulnerability = vulnerabilities![0]; @@ -200,7 +200,7 @@ public void GetKnownVulnerability_WithPackageVulnerableFromMultipleSources_Retur } }; - List? vulnerabilities = AuditUtility.GetKnownVulnerabilities("packageId", new NuGetVersion(2, 0, 0), knownVulnerabilities); + List? vulnerabilities = AuditChecker.GetKnownVulnerabilities("packageId", new NuGetVersion(2, 0, 0), knownVulnerabilities); vulnerabilities.Should().HaveCount(1); var moderateVulnerability = vulnerabilities![0]; @@ -210,7 +210,7 @@ public void GetKnownVulnerability_WithPackageVulnerableFromMultipleSources_Retur } [Fact] - public void FindPackagesWithKnownVulnerabilities_WithoutPackageNotVulnerable_ReturnsNull() + public void FindPackagesWithKnownVulnerabilities_WithPackageIdWithVulnerabilitiesButVersionNotVulnerable_ReturnsNull() { List>> knownVulnerabilities = new List>> @@ -234,40 +234,13 @@ public void FindPackagesWithKnownVulnerabilities_WithoutPackageNotVulnerable_Ret new PackageRestoreData(new PackageReference(packageIdentity, CommonFrameworks.Net472), new string[]{ }, isMissing: true) }; - AuditUtility.FindPackagesWithKnownVulnerabilities(knownVulnerabilities, packages, PackageVulnerabilitySeverity.Low).Should().BeNull(); + AuditChecker.FindPackagesWithKnownVulnerabilities(knownVulnerabilities, packages).Should().BeNull(); } [Fact] - public void FindPackagesWithKnownVulnerabilities_WithoutVulnerablePackageButLowerSeverity_ReturnsNull() - { - List>> knownVulnerabilities = - new List>> - { - new Dictionary>() - { - { "packageId", - new List { - new PackageVulnerabilityInfo( - new Uri("https://contoso.com/random-vulnerability1"), - PackageVulnerabilitySeverity.Moderate, - VersionRange.Parse("[1.0.0, 3.0.0)")) - } - } - } - }; - - PackageIdentity packageIdentity = new("packageId", new NuGetVersion(2, 0, 0)); - var packages = new List - { - new PackageRestoreData(new PackageReference(packageIdentity, CommonFrameworks.Net472), new string[]{ }, isMissing: true) - }; - - AuditUtility.FindPackagesWithKnownVulnerabilities(knownVulnerabilities, packages, PackageVulnerabilitySeverity.High).Should().BeNull(); - } - - [Fact] - public void FindPackagesWithKnownVulnerabilities_WithVulnerablePackageWithinSpecifiedSeverity_ReturnsAppropriatePackages() + public void FindPackagesWithKnownVulnerabilities_WithVulnerablePackage_ReturnsAppropriatePackages() { + var projectPath = "C:\\solution\\project\\project.csproj"; List>> knownVulnerabilities = new List>> { @@ -306,15 +279,17 @@ public void FindPackagesWithKnownVulnerabilities_WithVulnerablePackageWithinSpec PackageIdentity packageIdentity = new("packageId", new NuGetVersion(2, 0, 0)); var packages = new List { - new PackageRestoreData(new PackageReference(packageIdentity, CommonFrameworks.Net472), new string[]{ }, isMissing: true) + new PackageRestoreData(new PackageReference(packageIdentity, CommonFrameworks.Net472), new string[]{ projectPath }, isMissing: true) }; - var packagesWithVulnerabilities = AuditUtility.FindPackagesWithKnownVulnerabilities(knownVulnerabilities, packages, PackageVulnerabilitySeverity.Moderate); + var packagesWithVulnerabilities = AuditChecker.FindPackagesWithKnownVulnerabilities(knownVulnerabilities, packages); packagesWithVulnerabilities.Should().HaveCount(1); - (PackageIdentity vulnerablePackage, AuditUtility.PackageAuditInfo auditInfo) = packagesWithVulnerabilities.Single(); + (PackageIdentity vulnerablePackage, AuditChecker.PackageAuditInfo auditInfo) = packagesWithVulnerabilities.Single(); vulnerablePackage.Should().Be(packageIdentity); auditInfo.Identity.Should().Be(packageIdentity); auditInfo.Vulnerabilities.Should().HaveCount(2); + auditInfo.Projects.Should().HaveCount(1); + auditInfo.Projects.Should().Contain(projectPath); var moderateVulnerability = auditInfo.Vulnerabilities[0]; moderateVulnerability.Severity.Should().Be(PackageVulnerabilitySeverity.Moderate); @@ -335,7 +310,7 @@ public void FindPackagesWithKnownVulnerabilities_WithVulnerablePackageWithinSpec [InlineData(PackageVulnerabilitySeverity.Unknown, "unknown", NuGetLogCode.NU1900)] public void GetSeverityLabelAndCode_ReturnCorrectLabelAndCode(PackageVulnerabilitySeverity severity, string expectedLabel, NuGetLogCode expectedCode) { - AuditUtility.GetSeverityLabelAndCode(severity).Should().Be((expectedLabel, expectedCode)); + AuditChecker.GetSeverityLabelAndCode(severity).Should().Be((expectedLabel, expectedCode)); } internal class VulnerabilityInfoResourceImplementation : IVulnerabilityInfoResource @@ -384,8 +359,9 @@ public async Task GetAllVulnerabilityDataAsync_WithNoSourcesProvidingVulnerabili { new SourceRepository(new PackageSource("https://contoso.com/v3/index.json"), new List{new VulnerabilityInfoResourceProvider(vulnerabilityResults) }) }; - GetVulnerabilityInfoResult? vulnerabilityData = await AuditUtility.GetAllVulnerabilityDataAsync(sourceRepositories, Mock.Of(), NullLogger.Instance, CancellationToken.None); + (int count, GetVulnerabilityInfoResult? vulnerabilityData) = await AuditChecker.GetAllVulnerabilityDataAsync(sourceRepositories, Mock.Of(), NullLogger.Instance, CancellationToken.None); vulnerabilityData.Should().BeNull(); + count.Should().Be(0); } [Fact] @@ -417,12 +393,13 @@ public async Task GetAllVulnerabilityDataAsync_WithMultipleSources_AndSingleVuln new SourceRepository(new PackageSource(sourceWithVulnerabilityData), providers) }; - GetVulnerabilityInfoResult? vulnerabilityData = await AuditUtility.GetAllVulnerabilityDataAsync(sourceRepositories, Mock.Of(), NullLogger.Instance, CancellationToken.None); + (int count, GetVulnerabilityInfoResult? vulnerabilityData) = await AuditChecker.GetAllVulnerabilityDataAsync(sourceRepositories, Mock.Of(), NullLogger.Instance, CancellationToken.None); vulnerabilityData.Should().NotBeNull(); vulnerabilityData!.Exceptions.Should().BeNull(); vulnerabilityData.KnownVulnerabilities.Should().HaveCount(1); vulnerabilityData.KnownVulnerabilities.Single().Keys.Should().Contain("A"); vulnerabilityData.KnownVulnerabilities.Single().Values.Single().Should().HaveCount(1); + count.Should().Be(1); } [Fact] @@ -457,7 +434,8 @@ public async Task GetAllVulnerabilityDataAsync_WithMultipleSources_MergesDataAnd new SourceRepository(new PackageSource(sourceWithVulnerabilityData), providers) }; - GetVulnerabilityInfoResult? vulnerabilityData = await AuditUtility.GetAllVulnerabilityDataAsync(sourceRepositories, Mock.Of(), NullLogger.Instance, CancellationToken.None); + (int count, GetVulnerabilityInfoResult? vulnerabilityData) = await AuditChecker.GetAllVulnerabilityDataAsync(sourceRepositories, Mock.Of(), NullLogger.Instance, CancellationToken.None); + count.Should().Be(1); vulnerabilityData.Should().NotBeNull(); vulnerabilityData!.KnownVulnerabilities.Should().HaveCount(1); vulnerabilityData.KnownVulnerabilities.Single().Keys.Should().Contain("A"); @@ -510,13 +488,14 @@ public async Task GetAllVulnerabilityDataAsync_WithMultipleSources_MergesDataInO new SourceRepository(new PackageSource(sourceWithVulnerabilityData), providers) }; - GetVulnerabilityInfoResult? vulnerabilityData = await AuditUtility.GetAllVulnerabilityDataAsync(sourceRepositories, Mock.Of(), NullLogger.Instance, CancellationToken.None); + (int count, GetVulnerabilityInfoResult? vulnerabilityData) = await AuditChecker.GetAllVulnerabilityDataAsync(sourceRepositories, Mock.Of(), NullLogger.Instance, CancellationToken.None); vulnerabilityData.Should().NotBeNull(); vulnerabilityData!.KnownVulnerabilities.Should().HaveCount(2); vulnerabilityData.KnownVulnerabilities.First().Keys.Should().Contain("B"); vulnerabilityData.KnownVulnerabilities.First().Values.Single().Should().HaveCount(1); vulnerabilityData.KnownVulnerabilities.Last().Keys.Should().Contain("A"); vulnerabilityData.KnownVulnerabilities.Last().Values.Single().Should().HaveCount(1); + count.Should().Be(2); } [Fact] @@ -530,56 +509,465 @@ public async Task GetAllVulnerabilityDataAsync_SourceWithInvalidHost_ReturnResul using SourceCacheContext cacheContext = new(); // Act - GetVulnerabilityInfoResult? result = await AuditUtility.GetAllVulnerabilityDataAsync(sourceRepositories, cacheContext, NullLogger.Instance, CancellationToken.None); + (int count, GetVulnerabilityInfoResult? result) = await AuditChecker.GetAllVulnerabilityDataAsync(sourceRepositories, cacheContext, NullLogger.Instance, CancellationToken.None); // Assert result.Should().NotBeNull(); result!.KnownVulnerabilities.Should().BeNull(); result.Exceptions.Should().NotBeNull(); + count.Should().Be(0); + + } + + [Fact] + public void CreateWarnings_WithoutAuditSettings_WithoutProjectsInformation_ReturnsNull() + { + PackageIdentity packageIdentity = new("packageId", new NuGetVersion(2, 0, 0)); + AuditChecker.PackageAuditInfo packageAuditInfo = new(packageIdentity, Array.Empty()); + + packageAuditInfo.Vulnerabilities.Add(new PackageVulnerabilityInfo( + new Uri("https://contoso.com/random-vulnerability1"), + PackageVulnerabilitySeverity.Moderate, + VersionRange.Parse("[1.0.0, 3.0.0)"))); + + Dictionary result = new() + { + {packageIdentity, packageAuditInfo } + }; + + var auditSettings = new Dictionary(); + int Sev0Matches = 0, Sev1Matches = 0, Sev2Matches = 0, Sev3Matches = 0, InvalidSevMatches = 0; + List packagesWithReportedAdvisories = new List(); + + List warnings = AuditChecker.CreateWarnings(result!, + auditSettings, + ref Sev0Matches, + ref Sev1Matches, + ref Sev2Matches, + ref Sev3Matches, + ref InvalidSevMatches, + ref packagesWithReportedAdvisories); + + warnings.Should().BeEmpty(); + Sev0Matches.Should().Be(0); + Sev1Matches.Should().Be(0); + Sev2Matches.Should().Be(0); + Sev3Matches.Should().Be(0); + InvalidSevMatches.Should().Be(0); + packagesWithReportedAdvisories.Should().BeEmpty(); + } + + [Fact] + public void CreateWarnings_WithoutAuditSettings_RaisesWarningsForAllProjects() + { + PackageIdentity packageIdentity = new("packageId", new NuGetVersion(2, 0, 0)); + var projectPath1 = "C:\\solution\\project\\project1.csproj"; + var projectPath2 = "C:\\solution\\project\\project2.csproj"; + AuditChecker.PackageAuditInfo packageAuditInfo = new(packageIdentity, new string[] { projectPath1, projectPath2 }); + + packageAuditInfo.Vulnerabilities.Add(new PackageVulnerabilityInfo( + new Uri("https://contoso.com/random-vulnerability1"), + PackageVulnerabilitySeverity.Moderate, + VersionRange.Parse("[1.0.0, 3.0.0)"))); + + Dictionary result = new() + { + {packageIdentity, packageAuditInfo } + }; + + var auditSettings = new Dictionary(); + int Sev0Matches = 0, Sev1Matches = 0, Sev2Matches = 0, Sev3Matches = 0, InvalidSevMatches = 0; + List packagesWithReportedAdvisories = new List(); + + List warnings = AuditChecker.CreateWarnings(result!, + auditSettings, + ref Sev0Matches, + ref Sev1Matches, + ref Sev2Matches, + ref Sev3Matches, + ref InvalidSevMatches, + ref packagesWithReportedAdvisories); + + warnings.Should().HaveCount(2); + warnings[0].Code.Should().Be(NuGetLogCode.NU1902); + warnings[0].Message.Should().Be(string.Format(Strings.Warning_PackageWithKnownVulnerability, + packageIdentity.Id, + packageIdentity.Version.ToNormalizedString(), + PackageVulnerabilitySeverity.Moderate.ToString().ToLowerInvariant(), + packageAuditInfo.Vulnerabilities[0].Url)); + warnings[0].ProjectPath.Should().Be(projectPath1); + + warnings[1].Code.Should().Be(NuGetLogCode.NU1902); + warnings[1].Message.Should().Be(string.Format(Strings.Warning_PackageWithKnownVulnerability, + packageIdentity.Id, + packageIdentity.Version.ToNormalizedString(), + PackageVulnerabilitySeverity.Moderate.ToString().ToLowerInvariant(), + packageAuditInfo.Vulnerabilities[0].Url)); + warnings[1].ProjectPath.Should().Be(projectPath2); + + Sev0Matches.Should().Be(0); + Sev1Matches.Should().Be(1); + Sev2Matches.Should().Be(0); + Sev3Matches.Should().Be(0); + InvalidSevMatches.Should().Be(0); + packagesWithReportedAdvisories.Should().HaveCount(1); + packagesWithReportedAdvisories[0].Should().Be(packageIdentity); + } + + [Fact] + public void CreateWarnings_WithoutAuditSettings_RaisesWarningsForAllSeverities() + { + PackageIdentity packageIdentity = new("packageId", new NuGetVersion(2, 0, 0)); + var projectPath = "C:\\solution\\project\\project.csproj"; + AuditChecker.PackageAuditInfo packageAuditInfo = new(packageIdentity, new string[] { projectPath }); + + packageAuditInfo.Vulnerabilities.Add(new PackageVulnerabilityInfo( + new Uri("https://contoso.com/random-vulnerability1"), + PackageVulnerabilitySeverity.Moderate, + VersionRange.Parse("[1.0.0, 3.0.0)"))); + + Dictionary result = new() + { + {packageIdentity, packageAuditInfo } + }; + + var auditSettings = new Dictionary(); + int Sev0Matches = 0, Sev1Matches = 0, Sev2Matches = 0, Sev3Matches = 0, InvalidSevMatches = 0; + List packagesWithReportedAdvisories = new List(); + + List warnings = AuditChecker.CreateWarnings(result!, + auditSettings, + ref Sev0Matches, + ref Sev1Matches, + ref Sev2Matches, + ref Sev3Matches, + ref InvalidSevMatches, + ref packagesWithReportedAdvisories); + + warnings.Should().HaveCount(1); + warnings[0].Code.Should().Be(NuGetLogCode.NU1902); + warnings[0].Message.Should().Be(string.Format(Strings.Warning_PackageWithKnownVulnerability, + packageIdentity.Id, + packageIdentity.Version.ToNormalizedString(), + PackageVulnerabilitySeverity.Moderate.ToString().ToLowerInvariant(), + packageAuditInfo.Vulnerabilities[0].Url)); + warnings[0].ProjectPath.Should().Be(projectPath); + + Sev0Matches.Should().Be(0); + Sev1Matches.Should().Be(1); + Sev2Matches.Should().Be(0); + Sev3Matches.Should().Be(0); + InvalidSevMatches.Should().Be(0); + packagesWithReportedAdvisories.Should().HaveCount(1); + packagesWithReportedAdvisories[0].Should().Be(packageIdentity); + } + + [Fact] + public void CreateWarnings_WithVariousVulnerabilties_CountsSeverityMatchesCorrectly() + { + PackageIdentity packageA = new("a", new NuGetVersion(2, 0, 0)); + PackageIdentity packageB = new("b", new NuGetVersion(1, 0, 0)); + PackageIdentity packageC = new("c", new NuGetVersion(2, 5, 0)); + var projectPath = "C:\\solution\\project\\project.csproj"; + + AuditChecker.PackageAuditInfo packageAuditInfoA = new(packageA, new string[] { projectPath }); + packageAuditInfoA.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.High)); + packageAuditInfoA.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Low)); + packageAuditInfoA.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Low)); + packageAuditInfoA.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Critical)); + + AuditChecker.PackageAuditInfo packageAuditInfoB = new(packageB, new string[] { projectPath }); + packageAuditInfoB.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.High)); + packageAuditInfoB.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Moderate)); + packageAuditInfoB.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Moderate)); + + AuditChecker.PackageAuditInfo packageAuditInfoC = new(packageC, new string[] { projectPath }); + packageAuditInfoC.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Low)); + packageAuditInfoC.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Moderate)); + packageAuditInfoC.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Low)); + packageAuditInfoC.Vulnerabilities.Add(GetVulnerability(PackageVulnerabilitySeverity.Unknown)); + + Dictionary result = new() + { + {packageA, packageAuditInfoA }, + {packageB, packageAuditInfoB }, + {packageC, packageAuditInfoC }, + }; + + var auditSettings = new Dictionary(); + int Sev0Matches = 0, Sev1Matches = 0, Sev2Matches = 0, Sev3Matches = 0, InvalidSevMatches = 0; + List packagesWithReportedAdvisories = new List(); + + List warnings = AuditChecker.CreateWarnings(result!, + auditSettings, + ref Sev0Matches, + ref Sev1Matches, + ref Sev2Matches, + ref Sev3Matches, + ref InvalidSevMatches, + ref packagesWithReportedAdvisories); + + warnings.Should().HaveCount(11); + + Sev0Matches.Should().Be(4); + Sev1Matches.Should().Be(3); + Sev2Matches.Should().Be(2); + Sev3Matches.Should().Be(1); + InvalidSevMatches.Should().Be(1); + packagesWithReportedAdvisories.Should().HaveCount(3); + packagesWithReportedAdvisories[0].Should().Be(packageA); + packagesWithReportedAdvisories[1].Should().Be(packageB); + packagesWithReportedAdvisories[2].Should().Be(packageC); + static PackageVulnerabilityInfo GetVulnerability(PackageVulnerabilitySeverity packageVulnerabilitySeverity) + { + return new PackageVulnerabilityInfo( + new Uri($"https://contoso.com/{Guid.NewGuid()}"), + packageVulnerabilitySeverity, + VersionRange.Parse("[1.0.0, 3.0.0)")); + } + } + + [Fact] + public void CreateWarnings_WithAuditSettings_RaisesWarningsForEnabledProjectsOnly() + { + PackageIdentity packageIdentity = new("packageId", new NuGetVersion(2, 0, 0)); + var projectPath1 = "C:\\solution\\project\\project1.csproj"; + var projectPath2 = "C:\\solution\\project\\project2.csproj"; + AuditChecker.PackageAuditInfo packageAuditInfo = new(packageIdentity, new string[] { projectPath1, projectPath2 }); + + packageAuditInfo.Vulnerabilities.Add(new PackageVulnerabilityInfo( + new Uri("https://contoso.com/random-vulnerability1"), + PackageVulnerabilitySeverity.Moderate, + VersionRange.Parse("[1.0.0, 3.0.0)"))); + + Dictionary result = new() + { + {packageIdentity, packageAuditInfo } + }; + + var auditSettings = new Dictionary() + { + { projectPath1 , (true, PackageVulnerabilitySeverity.Moderate) }, + { projectPath2 , (false, PackageVulnerabilitySeverity.Moderate) } + }; + + int Sev0Matches = 0, Sev1Matches = 0, Sev2Matches = 0, Sev3Matches = 0, InvalidSevMatches = 0; + List packagesWithReportedAdvisories = new List(); + + List warnings = AuditChecker.CreateWarnings(result!, + auditSettings, + ref Sev0Matches, + ref Sev1Matches, + ref Sev2Matches, + ref Sev3Matches, + ref InvalidSevMatches, + ref packagesWithReportedAdvisories); + + warnings.Should().HaveCount(1); + warnings[0].Code.Should().Be(NuGetLogCode.NU1902); + warnings[0].Message.Should().Be(string.Format(Strings.Warning_PackageWithKnownVulnerability, + packageIdentity.Id, + packageIdentity.Version.ToNormalizedString(), + PackageVulnerabilitySeverity.Moderate.ToString().ToLowerInvariant(), + packageAuditInfo.Vulnerabilities[0].Url)); + warnings[0].ProjectPath.Should().Be(projectPath1); + + Sev0Matches.Should().Be(0); + Sev1Matches.Should().Be(1); + Sev2Matches.Should().Be(0); + Sev3Matches.Should().Be(0); + InvalidSevMatches.Should().Be(0); + packagesWithReportedAdvisories.Should().HaveCount(1); + packagesWithReportedAdvisories[0].Should().Be(packageIdentity); + } + + [Fact] + public void CreateWarnings_WithAuditSettings_RaisesWarningsForProjectsWithMatchingSeverityOnly() + { + PackageIdentity packageIdentity = new("packageId", new NuGetVersion(2, 0, 0)); + var projectPath1 = "C:\\solution\\project\\project1.csproj"; + var projectPath2 = "C:\\solution\\project\\project2.csproj"; + AuditChecker.PackageAuditInfo packageAuditInfo = new(packageIdentity, new string[] { projectPath1, projectPath2 }); + + packageAuditInfo.Vulnerabilities.Add(new PackageVulnerabilityInfo( + new Uri("https://contoso.com/random-vulnerability1"), + PackageVulnerabilitySeverity.Moderate, + VersionRange.Parse("[1.0.0, 3.0.0)"))); + + Dictionary result = new() + { + {packageIdentity, packageAuditInfo } + }; + + var auditSettings = new Dictionary() + { + { projectPath1 , (true, PackageVulnerabilitySeverity.Moderate) }, + { projectPath2 , (true, PackageVulnerabilitySeverity.High) } + }; + + int Sev0Matches = 0, Sev1Matches = 0, Sev2Matches = 0, Sev3Matches = 0, InvalidSevMatches = 0; + List packagesWithReportedAdvisories = new List(); + + List warnings = AuditChecker.CreateWarnings(result!, + auditSettings, + ref Sev0Matches, + ref Sev1Matches, + ref Sev2Matches, + ref Sev3Matches, + ref InvalidSevMatches, + ref packagesWithReportedAdvisories); + + warnings.Should().HaveCount(1); + warnings[0].Code.Should().Be(NuGetLogCode.NU1902); + warnings[0].Message.Should().Be(string.Format(Strings.Warning_PackageWithKnownVulnerability, + packageIdentity.Id, + packageIdentity.Version.ToNormalizedString(), + PackageVulnerabilitySeverity.Moderate.ToString().ToLowerInvariant(), + packageAuditInfo.Vulnerabilities[0].Url)); + warnings[0].ProjectPath.Should().Be(projectPath1); + + Sev0Matches.Should().Be(0); + Sev1Matches.Should().Be(1); + Sev2Matches.Should().Be(0); + Sev3Matches.Should().Be(0); + InvalidSevMatches.Should().Be(0); + packagesWithReportedAdvisories.Should().HaveCount(1); + packagesWithReportedAdvisories[0].Should().Be(packageIdentity); + } + + [Fact] + public async Task CheckVulnerabiltiesAsync_WithoutEnabledProjects_SkipsVulnerabilityCheckingAltogether() + { + SetupPrerequisites(out AuditChecker auditChecker, out string projectPath, out List packages); + + var restoreAuditProperties = new Dictionary() + { + { projectPath, new RestoreAuditProperties() + { + EnableAudit = "false" + } + } + }; + + AuditCheckResult result = await auditChecker.CheckPackageVulnerabilitiesAsync(packages, restoreAuditProperties, CancellationToken.None); + + result.Should().NotBeNull(); + result.Warnings.Should().BeEmpty(); + result.DownloadDurationInSeconds.Should().BeNull(); + result.CheckPackagesDurationInSeconds.Should().BeNull(); ; + + result.IsAuditEnabled.Should().BeFalse(); + result.SourcesWithVulnerabilities.Should().BeNull(); + result.Severity0VulnerabilitiesFound.Should().Be(0); + result.Severity1VulnerabilitiesFound.Should().Be(0); + result.Severity2VulnerabilitiesFound.Should().Be(0); + result.Severity3VulnerabilitiesFound.Should().Be(0); + result.InvalidSeverityVulnerabilitiesFound.Should().Be(0); + } + + [Fact] + public async Task CheckVulnerabiltiesAsync_WithSeverityLowerThanVulnerabilitiesReported_RaisesWarnings() + { + SetupPrerequisites(out AuditChecker auditChecker, out string projectPath, out List packages); + + var restoreAuditProperties = new Dictionary() + { + { projectPath, new RestoreAuditProperties() + { + EnableAudit = "true", + AuditLevel = "low", + } + } + }; + + AuditCheckResult result = await auditChecker.CheckPackageVulnerabilitiesAsync(packages, restoreAuditProperties, CancellationToken.None); + + result.Should().NotBeNull(); + result.Warnings.Should().HaveCount(1); + result.DownloadDurationInSeconds.Should().NotBeNull(); + result.CheckPackagesDurationInSeconds.Should().NotBeNull(); ; + + result.IsAuditEnabled.Should().BeTrue(); + result.SourcesWithVulnerabilities.Should().Be(1); + result.Severity0VulnerabilitiesFound.Should().Be(0); + result.Severity1VulnerabilitiesFound.Should().Be(1); + result.Severity2VulnerabilitiesFound.Should().Be(0); + result.Severity3VulnerabilitiesFound.Should().Be(0); + result.InvalidSeverityVulnerabilitiesFound.Should().Be(0); } [Fact] - public void CreateWarningsForPackagesWithVulnerabilities_CreatesWarningsForAllVulnerabilities() + public async Task CheckVulnerabiltiesAsync_WithSeverityHigherThanVulnerabilitiesReported_DoesNotRaiseWarnings() { - var packageA = new PackageIdentity("A", new NuGetVersion(1, 0, 0)); - var packageB = new PackageIdentity("B", new NuGetVersion(2, 0, 0)); + SetupPrerequisites(out AuditChecker auditChecker, out string projectPath, out List packages); + + var restoreAuditProperties = new Dictionary() + { + { projectPath, new RestoreAuditProperties() + { + EnableAudit = "true", + AuditLevel = "high", + } + } + }; - var pva = new PackageAuditInfo(packageA); - pva.Vulnerabilities.Add(new PackageVulnerabilityInfo(new Uri("https://vulnerability1"), PackageVulnerabilitySeverity.Low, VersionRange.Parse("[1.0.0,2.0.0)"))); - pva.Vulnerabilities.Add(new PackageVulnerabilityInfo(new Uri("https://vulnerability2"), PackageVulnerabilitySeverity.Moderate, VersionRange.Parse("[1.0.0,1.1.0)"))); - var pvb = new PackageAuditInfo(packageB); - pvb.Vulnerabilities.Add(new PackageVulnerabilityInfo(new Uri("https://vulnerability3"), PackageVulnerabilitySeverity.High, VersionRange.Parse("[2.0.0,3.0.0)"))); - pvb.Vulnerabilities.Add(new PackageVulnerabilityInfo(new Uri("https://vulnerability4"), PackageVulnerabilitySeverity.Critical, VersionRange.Parse("[2.0.0,2.1.0)"))); + AuditCheckResult result = await auditChecker.CheckPackageVulnerabilitiesAsync(packages, restoreAuditProperties, CancellationToken.None); - Dictionary packagesWithKnownVulnerabilities = new() + result.Should().NotBeNull(); + result.Warnings.Should().HaveCount(0); + result.DownloadDurationInSeconds.Should().NotBeNull(); + result.CheckPackagesDurationInSeconds.Should().NotBeNull(); ; + + result.IsAuditEnabled.Should().BeTrue(); + result.SourcesWithVulnerabilities.Should().Be(1); + result.Severity0VulnerabilitiesFound.Should().Be(0); + result.Severity1VulnerabilitiesFound.Should().Be(0); + result.Severity2VulnerabilitiesFound.Should().Be(0); + result.Severity3VulnerabilitiesFound.Should().Be(0); + result.InvalidSeverityVulnerabilitiesFound.Should().Be(0); + } + + // Setup a test bed with multiple sources, 1 source with vulnerabilities, 1 vulnerable package wih a moderate vulnerability. + private static void SetupPrerequisites(out AuditChecker auditChecker, out string projectPath, out List packages) + { + string packageId = "A"; + PackageIdentity packageIdentity = new(packageId, new NuGetVersion(1, 0, 0)); + projectPath = "C:\\solution\\project\\project.csproj"; + + List>> knownVulnerabilities = new List>>() { - { packageA, pva }, - { packageB, pvb } + new Dictionary> + { + { + packageId, + new PackageVulnerabilityInfo[] { + new PackageVulnerabilityInfo(new Uri("https://vulnerability1"), PackageVulnerabilitySeverity.Moderate, VersionRange.Parse("[1.0.0,2.0.0)")) + } + } + } + }; + + string sourceWithVulnerabilityData = "https://contoso.com/vulnerability/v3/index.json"; + Dictionary vulnerabilityResults = new() + { + { sourceWithVulnerabilityData, new GetVulnerabilityInfoResult(knownVulnerabilities, exceptions: null) } + }; + + var providers = new List { new VulnerabilityInfoResourceProvider(vulnerabilityResults) }; + var sourceRepositories = new List + { + new SourceRepository(new PackageSource("https://contoso.com/v3/index.json"), providers), + new SourceRepository(new PackageSource(sourceWithVulnerabilityData), providers) }; - var testLogger = new TestLogger(); - AuditUtility.CreateWarningsForPackagesWithVulnerabilities(packagesWithKnownVulnerabilities, testLogger); - testLogger.WarningMessages.Should().HaveCount(4); - testLogger.WarningMessages.Should().Contain(string.Format(Strings.Warning_PackageWithKnownVulnerability, - packageA.Id, - packageA.Version.ToNormalizedString(), - pva.Vulnerabilities[0].Severity.ToString().ToLower(), - pva.Vulnerabilities[0].Url)); - testLogger.WarningMessages.Should().Contain(string.Format(Strings.Warning_PackageWithKnownVulnerability, - packageA.Id, - packageA.Version.ToNormalizedString(), - pva.Vulnerabilities[1].Severity.ToString().ToLower(), - pva.Vulnerabilities[1].Url)); - testLogger.WarningMessages.Should().Contain(string.Format(Strings.Warning_PackageWithKnownVulnerability, - packageB.Id, - packageB.Version.ToNormalizedString(), - pvb.Vulnerabilities[0].Severity.ToString().ToLower(), - pvb.Vulnerabilities[0].Url)); - testLogger.WarningMessages.Should().Contain(string.Format(Strings.Warning_PackageWithKnownVulnerability, - packageB.Id, - packageB.Version.ToNormalizedString(), - pvb.Vulnerabilities[1].Severity.ToString().ToLower(), - pvb.Vulnerabilities[1].Url)); + var logger = new TestLogger(); + + auditChecker = new AuditChecker(sourceRepositories, new SourceCacheContext(), logger); + + packages = new List + { + new PackageRestoreData(new PackageReference(packageIdentity, CommonFrameworks.Net472), new string[]{ projectPath }, isMissing: true) + }; } } } diff --git a/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs b/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs index 2ac43b20cb0..0e185ebffab 100644 --- a/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs +++ b/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs @@ -10,6 +10,7 @@ using NuGet.Common; using NuGet.Packaging.Core; using NuGet.Protocol; +using NuGet.Versioning; namespace Test.Utility { @@ -18,16 +19,20 @@ public class FileSystemBackedV3MockServer : MockServer private string _packageDirectory; private readonly MockResponseBuilder _builder; private readonly bool _isPrivateFeed; - public FileSystemBackedV3MockServer(string packageDirectory, bool isPrivateFeed = false) + private readonly bool _sourceReportsVulnerabilities; + public FileSystemBackedV3MockServer(string packageDirectory, bool isPrivateFeed = false, bool sourceReportsVulnerabilities = false) { _packageDirectory = packageDirectory; _builder = new MockResponseBuilder(Uri.TrimEnd(new[] { '/' })); _isPrivateFeed = isPrivateFeed; InitializeServer(); + _sourceReportsVulnerabilities = sourceReportsVulnerabilities; } public ISet UnlistedPackages { get; } = new HashSet(); + public Dictionary> Vulnerabilities = new(); + public string ServiceIndexUri => _builder.GetV3Source(); private void InitializeServer() @@ -38,7 +43,10 @@ private void InitializeServer() { return new Action(response => { - var mockResponse = _builder.BuildV3IndexResponse(Uri); + var mockResponse = _sourceReportsVulnerabilities ? + _builder.BuildV3IndexResponseWithVulnerabilities(Uri) : + _builder.BuildV3IndexResponse(Uri); + response.ContentType = mockResponse.ContentType; SetResponseContent(response, mockResponse.Content); }); @@ -143,6 +151,31 @@ private Action ServerHandlerV3(HttpListenerRequest request }); } } + else if (path.StartsWith("/vulnerability/")) + { + if (path.EndsWith("index.json")) + { + return new Action(response => + { + response.ContentType = "application/json"; + var vulnerabilityJson = FeedUtilities.CreateVulnerabilitiesJson(Uri + "/vulnerability/vulnerability.json"); + SetResponseContent(response, vulnerabilityJson.ToString()); + }); + } + else if (path.EndsWith("/vulnerability.json")) + { + return new Action(response => + { + response.ContentType = "application/json"; + var vulnerabilityJson = FeedUtilities.CreateVulnerabilityForPackages(Vulnerabilities); + SetResponseContent(response, vulnerabilityJson.ToString()); + }); + } + else + { + throw new Exception("This test needs to be updated to support: " + path); + } + } else { throw new Exception("This test needs to be updated to support: " + path); diff --git a/test/TestUtilities/Test.Utility/MockResponses/FeedUtilities.cs b/test/TestUtilities/Test.Utility/MockResponses/FeedUtilities.cs index 1f9af895a84..704a79725eb 100644 --- a/test/TestUtilities/Test.Utility/MockResponses/FeedUtilities.cs +++ b/test/TestUtilities/Test.Utility/MockResponses/FeedUtilities.cs @@ -6,6 +6,8 @@ using System.Globalization; using System.Linq; using Newtonsoft.Json.Linq; +using NuGet.Protocol; +using NuGet.Versioning; namespace Test.Utility { @@ -174,5 +176,53 @@ private static JObject GetPackageRegistrationItem(string serverUri, string id, s return item; } + + public static void AddVulnerabilitiesResource(JObject index, string serverUri) + { + var resource = new JObject + { + { "@id", $"{serverUri}vulnerability/index.json" }, + { "@type", "VulnerabilityInfo/6.7.0" } + }; + + var array = index["resources"] as JArray; + array.Add(resource); + } + + public static JArray CreateVulnerabilitiesJson(string vulnerabilityJsonUri) + { + return JArray.Parse( + @"[ + { + ""@name"": ""all"", + ""@id"": """ + vulnerabilityJsonUri + @""", + ""@updated"": """ + DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture) + @""", + ""comment"": ""The data for vulnerabilities. Contains all vulnerabilities"" + } + ]"); + } + + public static JObject CreateVulnerabilityForPackages(Dictionary> packages) + { + var JObject = new JObject(); + + foreach (var package in packages) + { + var packageObject = new JArray(); + foreach (var vulnerability in package.Value) + { + var vulnerabilityObject = new JObject + { + new JProperty("url", vulnerability.Item1.ToString()), + new JProperty("severity", (int)vulnerability.Item2), + new JProperty("versions", vulnerability.Item3.ToNormalizedString()) + }; + packageObject.Add(vulnerabilityObject); + } + JObject.Add(package.Key, packageObject); + } + + return JObject; + } } } diff --git a/test/TestUtilities/Test.Utility/MockResponses/MockResponseBuilder.cs b/test/TestUtilities/Test.Utility/MockResponses/MockResponseBuilder.cs index ed63099a51e..abe5c586471 100644 --- a/test/TestUtilities/Test.Utility/MockResponses/MockResponseBuilder.cs +++ b/test/TestUtilities/Test.Utility/MockResponses/MockResponseBuilder.cs @@ -9,6 +9,7 @@ using System.Text; using System.Xml.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.Versioning; @@ -215,12 +216,22 @@ public MockResponse BuildFlatIndex(NuGetVersion version) }; } - public MockResponse BuildV3IndexResponse(string serverUri) + public MockResponse BuildV3IndexResponseWithVulnerabilities(string serverUri) { - var indexJson = FeedUtilities.CreateIndexJson(); + JObject indexJson = CreateMinimalIndexJson(serverUri); + FeedUtilities.AddVulnerabilitiesResource(indexJson, serverUri); - FeedUtilities.AddFlatContainerResource(indexJson, serverUri); - FeedUtilities.AddRegistrationResource(indexJson, serverUri); + return new MockResponse + { + ContentType = "text/javascript", + Content = Encoding.UTF8.GetBytes(indexJson.ToString()) + }; + + } + + public MockResponse BuildV3IndexResponse(string serverUri) + { + JObject indexJson = CreateMinimalIndexJson(serverUri); return new MockResponse { @@ -229,6 +240,15 @@ public MockResponse BuildV3IndexResponse(string serverUri) }; } + private static JObject CreateMinimalIndexJson(string serverUri) + { + var indexJson = FeedUtilities.CreateIndexJson(); + + FeedUtilities.AddFlatContainerResource(indexJson, serverUri); + FeedUtilities.AddRegistrationResource(indexJson, serverUri); + return indexJson; + } + public MockResponse BuildV2IndexResponse() { return new MockResponse diff --git a/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestSettingsContext.cs b/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestSettingsContext.cs index 9ae9308c7f9..dd2c9cd16bf 100644 --- a/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestSettingsContext.cs +++ b/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestSettingsContext.cs @@ -206,6 +206,12 @@ public void AddNetStandardFeeds() Save(); } + public void RemoveSource(string key) + { + RemoveSource(XML, key); + Save(); + } + public void AddSource(string sourceName, string sourceUri) { var section = GetOrAddSection(XML, "packageSources");