Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prefer SafeVersions property when considering other versions #11312

Merged
merged 3 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;

using NuGet.Versioning;

using NuGetUpdater.Core.Analyze;
Expand Down Expand Up @@ -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<object?[]> RequirementsFromIgnoredVersionsData()
{
yield return
Expand Down Expand Up @@ -82,4 +94,53 @@ public void RequirementsFromIgnoredVersions(string dependencyName, Condition[] i
}
];
}

public static IEnumerable<object[]> 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")],
}
]
}
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,22 @@ internal static Func<NuGetVersion, bool> 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));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new check. If no explicit safe versions are specified, the behavior remains unchanged. If there are, make sure one of them matches.

return versionGreaterThanCurrent
&& rangeSatisfies
&& prereleaseTypeMatches
&& !isIgnoredVersion
&& !isVulnerableVersion
&& isSafeVersion;
};
}

internal static Func<NuGetVersion, bool> CreateVersionFilter(NuGetVersion currentVersion)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public record Advisory
public ImmutableArray<Requirement>? AffectedVersions { get; init; } = null;
public ImmutableArray<Requirement>? PatchedVersions { get; init; } = null;
public ImmutableArray<Requirement>? UnaffectedVersions { get; init; } = null;

public IEnumerable<Requirement> SafeVersions => (PatchedVersions ?? []).Concat(UnaffectedVersions ?? []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -314,6 +308,30 @@ internal static ImmutableArray<Requirement> 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)
Expand Down
Loading