Skip to content

Commit

Permalink
Show Infobar when vulnerabilities are found after restore (#5258)
Browse files Browse the repository at this point in the history
* added info bar when vulnerabilities are found in restore
  • Loading branch information
martinrrm authored Aug 17, 2023
1 parent ee7b952 commit 4d815bf
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -25,12 +26,14 @@ internal interface ISolutionRestoreJob
/// There is not functional impact here, rather it's about telemetry reporting.</param>
/// <param name="logger">Logger.</param>
/// <param name="token">Cancellation token.</param>
/// <param name="vulnerabilitiesFoundService">InfoBar service.</param>
/// <returns>Result of restore operation. True if it succeeded.</returns>
Task<bool> ExecuteAsync(
SolutionRestoreRequest request,
SolutionRestoreJobContext jobContext,
RestoreOperationLogger logger,
Dictionary<string, object> restoreStartTrackingData,
Lazy<IVulnerabilitiesNotificationService> vulnerabilitiesFoundService,
CancellationToken token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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.

using System.Threading;
using System.Threading.Tasks;

#nullable enable

namespace NuGet.SolutionRestoreManager
{
public interface IVulnerabilitiesNotificationService
{
Task ReportVulnerabilitiesAsync(bool hasVulnerabilitiesInSolution, CancellationToken cancellationToken);
}
}

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

6 changes: 6 additions & 0 deletions src/NuGet.Clients/NuGet.SolutionRestoreManager/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@
<value>'{0}' is not an exact version like '[1.0.0]'. Only exact versions are allowed with PackageDownload.</value>
<comment>0 - the version string that's not exact</comment>
</data>
<data name="InfoBar_HyperlinkMessage" xml:space="preserve">
<value>Manage NuGet Packages</value>
</data>
<data name="InfoBar_TextMessage" xml:space="preserve">
<value>This solution contains packages with vulnerabilities.</value>
</data>
<data name="NothingToRestore" xml:space="preserve">
<value>All packages are already installed and there is nothing to restore.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ internal sealed class SolutionRestoreJob : ISolutionRestoreJob
private RestoreOperationLogger _logger;
private INuGetProjectContext _nuGetProjectContext;
private PackageRestoreConsent _packageRestoreConsent;
private Lazy<IVulnerabilitiesNotificationService> _vulnerabilitiesFoundService;

private NuGetOperationStatus _status;
private int _packageCount;
Expand Down Expand Up @@ -129,6 +130,7 @@ public async Task<bool> ExecuteAsync(
SolutionRestoreJobContext jobContext,
RestoreOperationLogger logger,
Dictionary<string, object> trackingData,
Lazy<IVulnerabilitiesNotificationService> vulnerabilitiesFoundService,
CancellationToken token)
{
if (request == null)
Expand All @@ -146,7 +148,13 @@ public async Task<bool> ExecuteAsync(
throw new ArgumentNullException(nameof(logger));
}

if (vulnerabilitiesFoundService == null)
{
throw new ArgumentNullException(nameof(vulnerabilitiesFoundService));
}

_logger = logger;
_vulnerabilitiesFoundService = vulnerabilitiesFoundService;

// update instance attributes with the shared context values
_nuGetProjectContext = jobContext.NuGetProjectContext;
Expand Down Expand Up @@ -512,6 +520,10 @@ await _logger.RunWithProgressAsync(
{
_status = NuGetOperationStatus.Failed;
}

// Display info bar in SolutionExplorer if there is a vulnerability during restore.
await _vulnerabilitiesFoundService.Value.ReportVulnerabilitiesAsync(AnyProjectHasVulnerablePackageWarning(restoreSummaries), t);

_nuGetProgressReporter.EndSolutionRestore(projectList);
}
},
Expand All @@ -525,6 +537,25 @@ await _logger.RunWithProgressAsync(
}
}

private bool AnyProjectHasVulnerablePackageWarning(IReadOnlyList<RestoreSummary> restoreSummaries)
{
foreach (RestoreSummary restoreSummary in restoreSummaries)
{
foreach (IRestoreLogMessage restoreLogMessage in restoreSummary.Errors)
{
if (restoreLogMessage.Code == NuGetLogCode.NU1901 ||
restoreLogMessage.Code == NuGetLogCode.NU1902 ||
restoreLogMessage.Code == NuGetLogCode.NU1903 ||
restoreLogMessage.Code == NuGetLogCode.NU1904)
{
return true;
}
}
}

return false;
}

// This event could be raised from multiple threads. Only perform thread-safe operations
private void PackageRestoreManager_PackageRestored(
object sender,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ internal sealed class SolutionRestoreWorker : SolutionEventsListener, ISolutionR
private readonly Lazy<IVsSolutionManager> _solutionManager;
private readonly Lazy<INuGetLockService> _lockService;
private readonly Lazy<Common.ILogger> _logger;
private readonly Lazy<IVulnerabilitiesNotificationService> _vulnerabilitiesFoundService;
private readonly AsyncLazy<IComponentModel> _componentModel;

private EnvDTE.SolutionEvents _solutionEvents;
Expand Down Expand Up @@ -97,14 +98,16 @@ public SolutionRestoreWorker(
Lazy<Common.ILogger> logger,
Lazy<INuGetErrorList> errorList,
Lazy<IOutputConsoleProvider> outputConsoleProvider,
Lazy<INuGetFeatureFlagService> nugetFeatureFlagService)
Lazy<INuGetFeatureFlagService> nugetFeatureFlagService,
Lazy<IVulnerabilitiesNotificationService> vulnerabilitiesFoundService)
: this(AsyncServiceProvider.GlobalProvider,
solutionManager,
lockService,
logger,
errorList,
outputConsoleProvider,
nugetFeatureFlagService)
nugetFeatureFlagService,
vulnerabilitiesFoundService)
{ }

public SolutionRestoreWorker(
Expand All @@ -114,7 +117,8 @@ public SolutionRestoreWorker(
Lazy<Common.ILogger> logger,
Lazy<INuGetErrorList> errorList,
Lazy<IOutputConsoleProvider> outputConsoleProvider,
Lazy<INuGetFeatureFlagService> nugetFeatureFlagService)
Lazy<INuGetFeatureFlagService> nugetFeatureFlagService,
Lazy<IVulnerabilitiesNotificationService> vulnerabilitiesFoundService)
{
if (asyncServiceProvider == null)
{
Expand Down Expand Up @@ -151,13 +155,19 @@ public SolutionRestoreWorker(
throw new ArgumentNullException(nameof(nugetFeatureFlagService));
}

if (vulnerabilitiesFoundService == null)
{
throw new ArgumentNullException(nameof(vulnerabilitiesFoundService));
}

_asyncServiceProvider = asyncServiceProvider;
_solutionManager = solutionManager;
_lockService = lockService;
_logger = logger;
_errorList = errorList;
_outputConsoleProvider = outputConsoleProvider;
_nugetFeatureFlagService = nugetFeatureFlagService;
_vulnerabilitiesFoundService = vulnerabilitiesFoundService;

var joinableTaskContextNode = new JoinableTaskContextNode(ThreadHelper.JoinableTaskContext);
_joinableCollection = joinableTaskContextNode.CreateCollection();
Expand Down Expand Up @@ -781,7 +791,7 @@ await logger.StartAsync(

// Run restore
var job = componentModel.GetService<ISolutionRestoreJob>();
return await job.ExecuteAsync(request, _restoreJobContext, logger, restoreStartTrackingData, jobCts.Token);
return await job.ExecuteAsync(request, _restoreJobContext, logger, restoreStartTrackingData, _vulnerabilitiesFoundService, jobCts.Token);
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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.ComponentModel.Composition;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using NuGet.VisualStudio;
using NuGet.VisualStudio.Telemetry;

namespace NuGet.SolutionRestoreManager
{
[Export(typeof(IVulnerabilitiesNotificationService))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class VulnerablePackagesInfoBar : IVulnerabilitiesNotificationService, IVsInfoBarUIEvents
{
private IAsyncServiceProvider _asyncServiceProvider = AsyncServiceProvider.GlobalProvider;
private IVsInfoBarUIElement? _infoBarUIElement;
private bool _infoBarVisible = false; // InfoBar is currently being displayed in the Solution Explorer
private bool _wasInfoBarClosed = false; // InfoBar was closed by the user, using the 'x'(close) in the InfoBar
private bool _wasInfoBarHidden = false; // InfoBar was hid, this is caused because there are no more vulnerabilities to address
private uint? _eventCookie; // To hold the connection cookie

[Import]
private Lazy<IPackageManagerLaunchService>? PackageManagerLaunchService { get; set; }

public async Task ReportVulnerabilitiesAsync(bool hasVulnerabilitiesInSolution, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

// If the infoBar was closed, don't show it for the rest of the VS session
// if the infobar is visible and there are vulnerabilities, no work needed
// if the infobar is not visible and there are no vulnerabilities, no work needed
if (_wasInfoBarClosed || (hasVulnerabilitiesInSolution == _infoBarVisible))
{
return;
}

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

// Hide the InfoBar if Vulnerabilities were fixed
if (!hasVulnerabilitiesInSolution && _infoBarVisible)
{
_wasInfoBarHidden = true;
_infoBarUIElement?.Close();
return;
}

try
{
await CreateInfoBar();

_infoBarVisible = true;
_wasInfoBarHidden = false;
}
catch (Exception ex)
{
await TelemetryUtility.PostFaultAsync(ex, nameof(VulnerablePackagesInfoBar));
return;
}
}

private async Task CreateInfoBar()
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

// Initialize the InfoBar host in the SolutionExplorer window
IVsInfoBarHost? infoBarHost;
IVsUIShell? uiShell = await _asyncServiceProvider.GetServiceAsync<SVsUIShell, IVsUIShell>(throwOnFailure: true);
int windowFrameCode = uiShell!.FindToolWindow((uint)__VSFINDTOOLWIN.FTW_fFindFirst, VSConstants.StandardToolWindows.SolutionExplorer, out var windowFrame);
if (ErrorHandler.Failed(windowFrameCode))
{
Exception exception = new Exception(string.Format(CultureInfo.CurrentCulture, "Unable to find Solution Explorer window. HRRESULT {0}", windowFrameCode));
await TelemetryUtility.PostFaultAsync(exception, nameof(VulnerablePackagesInfoBar));
return;
}

object tempObject;
int hostBarCode = windowFrame.GetProperty((int)__VSFPROPID7.VSFPROPID_InfoBarHost, out tempObject);
if (ErrorHandler.Failed(hostBarCode))
{
Exception exception = new Exception(string.Format(CultureInfo.CurrentCulture, "Unable to find InfoBarHost. HRRESULT {0}", hostBarCode));
await TelemetryUtility.PostFaultAsync(exception, nameof(VulnerablePackagesInfoBar));
return;
}

infoBarHost = (IVsInfoBarHost)tempObject;

// Create the VulnerabilitiesFound InfoBar
IVsInfoBarUIFactory? infoBarFactory = await _asyncServiceProvider.GetServiceAsync<SVsInfoBarUIFactory, IVsInfoBarUIFactory>(throwOnFailure: false);
if (infoBarFactory == null)
{
NullReferenceException exception = new NullReferenceException(nameof(infoBarFactory));
await TelemetryUtility.PostFaultAsync(exception, nameof(VulnerablePackagesInfoBar));
return;
}

InfoBarModel infoBarModel = GetInfoBarModel();

_infoBarUIElement = infoBarFactory.CreateInfoBar(infoBarModel);
_infoBarUIElement.Advise(this, out uint cookie);
_eventCookie = cookie;

infoBarHost.AddInfoBar(_infoBarUIElement);
}

public void OnClosed(IVsInfoBarUIElement infoBarUIElement)
{
ThreadHelper.ThrowIfNotOnUIThread();

if (_eventCookie.HasValue)
{
infoBarUIElement?.Unadvise(_eventCookie.Value);
infoBarUIElement?.Close();
_eventCookie = null;
}

_infoBarVisible = false;

if (!_wasInfoBarHidden)
{
_wasInfoBarClosed = true;
}
}

public void OnActionItemClicked(IVsInfoBarUIElement infoBarUIElement, IVsInfoBarActionItem actionItem)
{
ThreadHelper.ThrowIfNotOnUIThread();
PackageManagerLaunchService?.Value.LaunchSolutionPackageManager();
}

protected InfoBarModel GetInfoBarModel()
{
return new InfoBarModel(
Resources.InfoBar_TextMessage,
new IVsInfoBarActionItem[]
{
new InfoBarHyperlink(Resources.InfoBar_HyperlinkMessage),
},
KnownMonikers.StatusWarning);
}
}
}
37 changes: 37 additions & 0 deletions src/NuGet.Clients/NuGet.Tools/PackageManagerLaunchService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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.

using System;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using NuGet.VisualStudio;
using NuGet.VisualStudio.Telemetry;

#nullable enable

namespace NuGetVSExtension
{
[Export(typeof(IPackageManagerLaunchService))]
[PartCreationPolicy(CreationPolicy.Shared)]
internal class PackageManagerLaunchService : IPackageManagerLaunchService
{
public void LaunchSolutionPackageManager()
{
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(async delegate
{
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
IVsUIShell vsUIShell = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<IVsUIShell, IVsUIShell>();

object targetGuid = Guid.Empty;
var guidNuGetDialog = GuidList.guidNuGetDialogCmdSet;
vsUIShell.PostExecCommand(
ref guidNuGetDialog,
(uint)PkgCmdIDList.cmdidAddPackageDialogForSolution,
(uint)0,
ref targetGuid);
}).PostOnFailure(nameof(PackageManagerLaunchService));
}
}
}
Loading

0 comments on commit 4d815bf

Please sign in to comment.