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

[Transitive's in Solution] Vulnerability indicators in solution level details pane for top-level packages #6010

Merged
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
@@ -9,16 +9,20 @@

namespace NuGet.PackageManagement.UI
{
public class DownloadCountToVisibilityConverter : IValueConverter
public class IsGreaterThanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType == typeof(Visibility))
{
long? downloadCount = value as long?;
if (downloadCount >= 0)
if (value is not null)
{
return Visibility.Visible;
long intValue = System.Convert.ToInt64(value, culture);
long parameterValue = System.Convert.ToInt64(parameter, culture);
if (intValue > parameterValue)
{
return Visibility.Visible;
}
}

return Visibility.Collapsed;
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ internal class PackageSolutionDetailControlModel : DetailControlModel
private INuGetSolutionManagerService _solutionManager;
private string _installedVersions; // The text describing the installed versions information, such as "not installed", "multiple versions installed" etc.
private int _installedVersionsCount; // the count of different installed versions
private int _installedVulnerabilitiesCount; // the count of distinct vulnerable installed versions
private bool _canUninstall;
private bool _canInstall;
// Indicates whether the SelectCheckBoxState is being updated in code. True means the state is being updated by code, while false means the state is changed by user clicking the checkbox.
@@ -119,7 +120,8 @@ public string InstalledVersions

private async Task UpdateInstalledVersionsAsync(CancellationToken cancellationToken)
{
var hash = new HashSet<NuGetVersion>();
var installedVersionsSet = new HashSet<NuGetVersion>();
var vulnerabilitiesSet = new HashSet<NuGetVersion>();

foreach (var project in _projects)
{
@@ -129,19 +131,26 @@ private async Task UpdateInstalledVersionsAsync(CancellationToken cancellationTo
if (installedVersion != null)
{
project.InstalledVersion = installedVersion.Identity.Version;
hash.Add(installedVersion.Identity.Version);
installedVersionsSet.Add(installedVersion.Identity.Version);
project.AutoReferenced = installedVersion.IsAutoReferenced;

if (project.NuGetProject.ProjectStyle.Equals(ProjectStyle.PackageReference))
{
project.RequestedVersion = installedVersion?.AllowedVersions?.OriginalString;
}

if (_searchResultPackage.VulnerableVersions.TryGetValue(installedVersion.Identity.Version, out int vulnerable))
{
project.InstalledVersionMaxVulnerability = vulnerable;
vulnerabilitiesSet.Add(installedVersion.Identity.Version);
}
}
else
{
project.RequestedVersion = null;
project.InstalledVersion = null;
project.AutoReferenced = false;
project.InstalledVersionMaxVulnerability = -1;
}
}
catch (Exception ex)
@@ -155,16 +164,17 @@ private async Task UpdateInstalledVersionsAsync(CancellationToken cancellationTo
}
}

InstalledVersionsCount = hash.Count;
InstalledVersionsCount = installedVersionsSet.Count;
InstalledVulnerabilitiesCount = vulnerabilitiesSet.Count;

if (hash.Count == 0)
if (installedVersionsSet.Count == 0)
{
InstalledVersions = Resources.Text_NotInstalled;
}
else if (hash.Count == 1)
else if (installedVersionsSet.Count == 1)
{
var displayVersion = new DisplayVersion(
hash.First(),
installedVersionsSet.First(),
string.Empty);
InstalledVersions = displayVersion.ToString();
}
@@ -276,6 +286,16 @@ public int InstalledVersionsCount
}
}

public int InstalledVulnerabilitiesCount
{
get => _installedVulnerabilitiesCount;
set
{
_installedVulnerabilitiesCount = value;
OnPropertyChanged(nameof(InstalledVulnerabilitiesCount));
}
}

// The event handler that is called when a project is added, removed or renamed.
private void SolutionProjectChanged(object sender, IProjectContextInfo project)
{
Original file line number Diff line number Diff line change
@@ -49,6 +49,26 @@ private async ValueTask InitializeAsync(CancellationToken cancellationToken)
private NuGetVersion _versionInstalled;
private string _versionRequested;

private int _installedVersionMaxVulnerability = -1;
public int InstalledVersionMaxVulnerability
{
get => _installedVersionMaxVulnerability;
set
{
if (_installedVersionMaxVulnerability != value)
{
_installedVersionMaxVulnerability = value;
OnPropertyChanged(nameof(InstalledVersionMaxVulnerability));
OnPropertyChanged(nameof(IsInstalledVersionVulnerable));
}
}
}

public bool IsInstalledVersionVulnerable
{
get => InstalledVersionMaxVulnerability > -1;
}

public NuGetVersion InstalledVersion
{
get { return _versionInstalled; }
Original file line number Diff line number Diff line change
@@ -280,6 +280,7 @@ public IEnumerable<PackageItemViewModel> GetCurrent()
existingListItem.TransitiveOrigins.AddRange(metadataContextInfo.TransitiveOrigins);
existingListItem.TransitiveToolTipMessage = string.Format(CultureInfo.CurrentCulture, Resources.PackageVersionWithTransitiveOrigins, string.Join(", ", existingListItem.InstalledVersions), string.Join(", ", existingListItem.TransitiveOrigins));
}
existingListItem.UpdateInstalledPackagesVulnerabilities(new PackageIdentity(packageId, packageVersion));
}
else
{
18 changes: 18 additions & 0 deletions src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.resx
Original file line number Diff line number Diff line change
@@ -1060,4 +1060,12 @@ Please see https://aka.ms/troubleshoot_nuget_cache for more help.</value>
<data name="Hyperlink_Owner" xml:space="preserve">
<value>Owner</value>
</data>
<data name="Label_InstalledVulnerabilitiesCount" xml:space="preserve">
<value>{0} vulnerable versions installed</value>
<comment>{0} is the number of installed vulnerable versions</comment>
</data>
<data name="Label_InstalledVulnerabilitiesCountSingle" xml:space="preserve">
<value>{0} vulnerable version installed</value>
<comment>{0} is the number of installed vulnerable versions</comment>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -40,8 +40,8 @@
VisibilityIfFalse="Visible" />
<vsui:BooleanToHiddenVisibilityConverter
x:Key="BooleanToHiddenVisibilityConverter" />
<nuget:DownloadCountToVisibilityConverter
x:Key="DownloadCountToVisibilityConverter" />
<nuget:IsGreaterThanToVisibilityConverter
x:Key="IsGreaterThanToVisibilityConverter" />
<nuget:FontSizeConverter
Scale="122"
x:Key="Font122PercentSizeConverter" />
Original file line number Diff line number Diff line change
@@ -152,8 +152,8 @@ public ObservableCollection<NuGetVersion> InstalledVersions
}
}

private IEnumerable<NuGetVersion> _vulnerableVersions = [];
public IEnumerable<NuGetVersion> VulnerableVersions
private Dictionary<NuGetVersion, int> _vulnerableVersions = [];
public Dictionary<NuGetVersion, int> VulnerableVersions
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 were mistakenly added in the previous PR and were never used.

{
get
{
@@ -836,32 +836,53 @@ private async System.Threading.Tasks.Task ReloadPackageVersionsAsync()
}
}

private async Task ReloadPackageMetadataAsync()
private async Task ReloadTopLevelPackageMetadataAsync()
{
CancellationToken cancellationToken = _cancellationTokenSource.Token;
try
{
var identity = new PackageIdentity(Id, Version);

if (PackageLevel == PackageLevel.TopLevel)
{
(PackageSearchMetadataContextInfo packageMetadata, PackageDeprecationMetadataContextInfo deprecationMetadata) =
await _searchService.GetPackageMetadataAsync(identity, Sources, IncludePrerelease, cancellationToken);
(PackageSearchMetadataContextInfo packageMetadata, PackageDeprecationMetadataContextInfo deprecationMetadata) =
await _searchService.GetPackageMetadataAsync(identity, Sources, IncludePrerelease, cancellationToken);

await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

DeprecationMetadata = deprecationMetadata;
IsPackageDeprecated = deprecationMetadata != null;

DeprecationMetadata = deprecationMetadata;
IsPackageDeprecated = deprecationMetadata != null;
VulnerabilityMaxSeverity = packageMetadata?.Vulnerabilities?.FirstOrDefault()?.Severity ?? -1;
SetVulnerabilityMaxSeverity(identity.Version, packageMetadata?.Vulnerabilities?.FirstOrDefault()?.Severity ?? -1);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// UI requested cancellation.
}
catch (TaskCanceledException)
{
// HttpClient throws TaskCanceledExceptions for HTTP timeouts
try
{
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
IsPackageWithNetworkErrors = true;
}
else if (PackageLevel == PackageLevel.Transitive && _vulnerabilityService != null)
catch (OperationCanceledException)
{
IEnumerable<PackageVulnerabilityMetadataContextInfo> vulnerabilityInfoList =
await _vulnerabilityService.GetVulnerabilityInfoAsync(identity, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
// if cancellationToken cancelled before the above is scheduled on UI thread, don't log fault telemetry
}
}
}

VulnerabilityMaxSeverity = vulnerabilityInfoList?.FirstOrDefault()?.Severity ?? -1;
private async Task ReloadTransitivePackageMetadataAsync()
{
CancellationToken cancellationToken = _cancellationTokenSource.Token;

try
{
if (_vulnerabilityService != null)
{
var identity = new PackageIdentity(Id, Version);
await UpdatePackageMaxVulnerabilityAsync(identity, cancellationToken);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
@@ -883,6 +904,36 @@ private async Task ReloadPackageMetadataAsync()
}
}

private async Task UpdatePackageMaxVulnerabilityAsync(PackageIdentity packageIdentity, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

IEnumerable<PackageVulnerabilityMetadataContextInfo> vulnerabilityInfoList =
await _vulnerabilityService.GetVulnerabilityInfoAsync(packageIdentity, cancellationToken);

SetVulnerabilityMaxSeverity(packageIdentity.Version, vulnerabilityInfoList?.FirstOrDefault()?.Severity ?? -1);
}

private void SetVulnerabilityMaxSeverity(NuGetVersion version, int maxSeverity)
{
if (maxSeverity > -1)
{
VulnerableVersions.Add(version, maxSeverity);
VulnerabilityMaxSeverity = Math.Max(VulnerabilityMaxSeverity, maxSeverity);

OnPropertyChanged(nameof(Status));
}
}

public void UpdateInstalledPackagesVulnerabilities(PackageIdentity packageIdentity)
{
CancellationToken cancellationToken = _cancellationTokenSource.Token;

NuGetUIThreadHelper.JoinableTaskFactory
.RunAsync(() => UpdatePackageMaxVulnerabilityAsync(packageIdentity, cancellationToken))
.PostOnFailure(nameof(PackageItemViewModel), nameof(UpdatePackageMaxVulnerabilityAsync));
}

public void UpdatePackageStatus(IEnumerable<PackageCollectionItem> installedPackages)
{
// Get the maximum version installed in any target project/solution
@@ -898,22 +949,22 @@ public void UpdatePackageStatus(IEnumerable<PackageCollectionItem> installedPack
.PostOnFailure(nameof(PackageItemViewModel), nameof(ReloadPackageVersionsAsync));

NuGetUIThreadHelper.JoinableTaskFactory
.RunAsync(ReloadPackageMetadataAsync)
.PostOnFailure(nameof(PackageItemViewModel), nameof(ReloadPackageMetadataAsync));
.RunAsync(ReloadTopLevelPackageMetadataAsync)
.PostOnFailure(nameof(PackageItemViewModel), nameof(ReloadTopLevelPackageMetadataAsync));

OnPropertyChanged(nameof(Status));
}

public void UpdateTransitivePackageStatus(NuGetVersion installedVersion)
{
InstalledVersion = installedVersion ?? throw new ArgumentNullException(nameof(installedVersion)); ;
InstalledVersion = installedVersion ?? throw new ArgumentNullException(nameof(installedVersion));

// Transitive packages cannot be updated and can only be installed as top-level packages with their currently installed version.
LatestVersion = installedVersion;

NuGetUIThreadHelper.JoinableTaskFactory
.RunAsync(ReloadPackageMetadataAsync)
.PostOnFailure(nameof(PackageItemViewModel), nameof(ReloadPackageMetadataAsync));
.RunAsync(ReloadTransitivePackageMetadataAsync)
.PostOnFailure(nameof(PackageItemViewModel), nameof(ReloadTransitivePackageMetadataAsync));

OnPropertyChanged(nameof(Status));
}
Original file line number Diff line number Diff line change
@@ -260,7 +260,7 @@

<!-- downloads -->
<TextBlock
Visibility="{Binding Path=DownloadCount,Converter={StaticResource DownloadCountToVisibilityConverter}}"
Visibility="{Binding Path=DownloadCount,Converter={StaticResource IsGreaterThanToVisibilityConverter},ConverterParameter=-1}"
Grid.Row="5"
Grid.Column="0"
FontWeight="Bold"
@@ -269,7 +269,7 @@
Text="{x:Static nuget:Resources.Label_Downloads}" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=DownloadCount,Converter={StaticResource DownloadCountToVisibilityConverter}}"
Visibility="{Binding Path=DownloadCount,Converter={StaticResource IsGreaterThanToVisibilityConverter},ConverterParameter=-1}"
AutomationProperties.LabeledBy="{Binding ElementName=_downloadsLabel}"
Text="{Binding Path=DownloadCount,StringFormat={}{0:N0}}"
Margin="8,8,0,0"
Loading