diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs index 0b546c97c3..669207d6f9 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -478,6 +478,61 @@ await TestAnalyzeAsync( ); } + [Fact] + public async Task SafeVersionsPropertyIsHonored() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0"), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.1.0", "net8.0"), // should update to this due to `SafeVersions` + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.2.0", "net8.0"), // this should not be considered + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Package", "1.0.0", DependencyType.PackageReference), + ], + ReferencedProjectPaths = [], + ImportedFiles = [], + AdditionalFiles = [], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "1.0.0", + IgnoredVersions = [], + IsVulnerable = false, + Vulnerabilities = [ + new() + { + DependencyName = "Some.Package", + PackageManager = "nuget", + VulnerableVersions = [Requirement.Parse(">= 1.0.0, < 1.1.0")], + SafeVersions = [Requirement.Parse("= 1.1.0")] + } + ], + }, + expectedResult: new() + { + UpdatedVersion = "1.1.0", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [ + new("Some.Package", "1.1.0", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + [Fact] public async Task VersionFinderCanHandle404FromPackageSource_V2() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs index 17db05d563..b21fe1d432 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + using NuGet.Versioning; using NuGetUpdater.Core.Analyze; @@ -29,6 +31,16 @@ public void RequirementsFromIgnoredVersions(string dependencyName, Condition[] i Assert.Equal(expectedRequirementsStrings, actualRequirementsStrings); } + [Theory] + [MemberData(nameof(DependencyInfoFromJobData))] + public void DependencyInfoFromJob(Job job, Dependency dependency, DependencyInfo expectedDependencyInfo) + { + var actualDependencyInfo = RunWorker.GetDependencyInfo(job, dependency); + var expectedString = JsonSerializer.Serialize(expectedDependencyInfo, AnalyzeWorker.SerializerOptions); + var actualString = JsonSerializer.Serialize(actualDependencyInfo, AnalyzeWorker.SerializerOptions); + Assert.Equal(expectedString, actualString); + } + public static IEnumerable RequirementsFromIgnoredVersionsData() { yield return @@ -82,4 +94,53 @@ public void RequirementsFromIgnoredVersions(string dependencyName, Condition[] i } ]; } + + public static IEnumerable DependencyInfoFromJobData() + { + yield return + [ + // job + new Job() + { + Source = new() + { + Provider = "github", + Repo = "some/repo" + }, + SecurityAdvisories = [ + new() + { + DependencyName = "Some.Dependency", + AffectedVersions = [Requirement.Parse(">= 1.0.0, < 1.1.0")], + PatchedVersions = [Requirement.Parse("= 1.1.0")], + UnaffectedVersions = [Requirement.Parse("= 1.2.0")] + }, + new() + { + DependencyName = "Unrelated.Dependency", + AffectedVersions = [Requirement.Parse(">= 1.0.0, < 99.99.99")] + } + ] + }, + // dependency + new Dependency("Some.Dependency", "1.0.0", DependencyType.PackageReference), + // expectedDependencyInfo + new DependencyInfo() + { + Name = "Some.Dependency", + Version = "1.0.0", + IsVulnerable = true, + IgnoredVersions = [], + Vulnerabilities = [ + new() + { + DependencyName = "Some.Dependency", + PackageManager = "nuget", + VulnerableVersions = [Requirement.Parse(">= 1.0.0, < 1.1.0")], + SafeVersions = [Requirement.Parse("= 1.1.0"), Requirement.Parse("= 1.2.0")], + } + ] + } + ]; + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs index cf0555a9db..7a8f7160e4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -113,11 +113,22 @@ internal static Func CreateVersionFilter(DependencyInfo depe ? versionRange.MinVersion : null; - return version => (currentVersion is null || version > currentVersion) - && versionRange.Satisfies(version) - && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) - && !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version)) - && !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); + var safeVersions = dependencyInfo.Vulnerabilities.SelectMany(v => v.SafeVersions).ToList(); + return version => + { + var versionGreaterThanCurrent = currentVersion is null || version > currentVersion; + var rangeSatisfies = versionRange.Satisfies(version); + var prereleaseTypeMatches = currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version; + var isIgnoredVersion = dependencyInfo.IgnoredVersions.Any(i => i.IsSatisfiedBy(version)); + var isVulnerableVersion = dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); + var isSafeVersion = !safeVersions.Any() || safeVersions.Any(s => s.IsSatisfiedBy(version)); + return versionGreaterThanCurrent + && rangeSatisfies + && prereleaseTypeMatches + && !isIgnoredVersion + && !isVulnerableVersion + && isSafeVersion; + }; } internal static Func CreateVersionFilter(NuGetVersion currentVersion) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Advisory.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Advisory.cs index eb052eca40..1fddd91f71 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Advisory.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Advisory.cs @@ -10,4 +10,6 @@ public record Advisory public ImmutableArray? AffectedVersions { get; init; } = null; public ImmutableArray? PatchedVersions { get; init; } = null; public ImmutableArray? UnaffectedVersions { get; init; } = null; + + public IEnumerable SafeVersions => (PatchedVersions ?? []).Concat(UnaffectedVersions ?? []); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs index 7bce84a7e6..35727ab217 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs @@ -4,6 +4,8 @@ using System.Text.Json; using System.Text.Json.Serialization; +using NuGet.Versioning; + using NuGetUpdater.Core.Analyze; using NuGetUpdater.Core.Discover; using NuGetUpdater.Core.Run.ApiModel; @@ -164,15 +166,7 @@ async Task TrackOriginalContentsAsync(string directory, string fileName) continue; } - var ignoredVersions = GetIgnoredRequirementsForDependency(job, dependency.Name); - var dependencyInfo = new DependencyInfo() - { - Name = dependency.Name, - Version = dependency.Version!, - IsVulnerable = false, - IgnoredVersions = ignoredVersions, - Vulnerabilities = [], - }; + var dependencyInfo = GetDependencyInfo(job, dependency); var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo); // TODO: log analysisResult if (analysisResult.CanUpdate) @@ -314,6 +308,30 @@ internal static ImmutableArray GetIgnoredRequirementsForDependency( return ignoredVersions; } + internal static DependencyInfo GetDependencyInfo(Job job, Dependency dependency) + { + var dependencyVersion = NuGetVersion.Parse(dependency.Version!); + var securityAdvisories = job.SecurityAdvisories.Where(s => s.DependencyName.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase)).ToArray(); + var isVulnerable = securityAdvisories.Any(s => (s.AffectedVersions ?? []).Any(v => v.IsSatisfiedBy(dependencyVersion))); + var ignoredVersions = GetIgnoredRequirementsForDependency(job, dependency.Name); + var vulnerabilities = securityAdvisories.Select(s => new SecurityVulnerability() + { + DependencyName = dependency.Name, + PackageManager = "nuget", + VulnerableVersions = s.AffectedVersions ?? [], + SafeVersions = s.SafeVersions.ToImmutableArray(), + }).ToImmutableArray(); + var dependencyInfo = new DependencyInfo() + { + Name = dependency.Name, + Version = dependencyVersion.ToString(), + IsVulnerable = isVulnerable, + IgnoredVersions = ignoredVersions, + Vulnerabilities = vulnerabilities, + }; + return dependencyInfo; + } + internal static UpdatedDependencyList GetUpdatedDependencyListFromDiscovery(WorkspaceDiscoveryResult discoveryResult, string pathToContents) { string GetFullRepoPath(string path)