Skip to content

Commit

Permalink
NuGetAuditSuppress for packages.config in VS (#5918)
Browse files Browse the repository at this point in the history
  • Loading branch information
zivkan committed Jul 29, 2024
1 parent 0c714c1 commit 8e96cf5
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,11 @@ public Task<IEnumerable<LibraryDependency>> GetPackageReferencesAsync(
{
throw new NotSupportedException();
}

public async Task<IReadOnlyList<(string id, string[] metadata)>> GetItemsAsync(string itemTypeName, params string[] metadataNames)
{
await _threadingService.JoinableTaskFactory.SwitchToMainThreadAsync();
return VsManagedLanguagesProjectSystemServices.GetItems(_vsProjectAdapter, itemTypeName, metadataNames);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,11 @@ public Task<IEnumerable<LibraryDependency>> GetPackageReferencesAsync(
{
throw new NotSupportedException();
}

public async Task<IReadOnlyList<(string id, string[] metadata)>> GetItemsAsync(string itemTypeName, params string[] metadataNames)
{
await _threadingService.JoinableTaskFactory.SwitchToMainThreadAsync();
return VsManagedLanguagesProjectSystemServices.GetItems(_vsProjectAdapter, itemTypeName, metadataNames);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,11 @@ public Task<IEnumerable<LibraryDependency>> GetPackageReferencesAsync(
{
throw new NotSupportedException();
}

public async Task<IReadOnlyList<(string id, string[] metadata)>> GetItemsAsync(string itemTypeName, params string[] metadataNames)
{
await _threadingService.JoinableTaskFactory.SwitchToMainThreadAsync();
return VsManagedLanguagesProjectSystemServices.GetItems(_vsProjectAdapter, itemTypeName, metadataNames);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,33 @@ private bool IsCentralPackageManagementVersionsEnabled()
#pragma warning restore CS0618 // Type or member is obsolete
}

public async Task<IReadOnlyList<(string id, string[] metadata)>> GetItemsAsync(string itemTypeName, params string[] metadataNames)
{
await _threadingService.JoinableTaskFactory.SwitchToMainThreadAsync();
return GetItems(_vsProjectAdapter, itemTypeName, metadataNames);
}

internal static IReadOnlyList<(string id, string[] metadata)> GetItems(IVsProjectAdapter projectAdapter, string itemTypeName, params string[] metadataNames)
{
ThreadHelper.ThrowIfNotOnUIThread();

IEnumerable<(string ItemId, string[] ItemMetadata)> items = projectAdapter.GetBuildItemInformation(itemTypeName, metadataNames);
var enumerator = items.GetEnumerator();
if (!enumerator.MoveNext())
{
return Array.Empty<(string, string[])>();
}

List<(string, string[])> result = items is ICollection<(string, string[])> itemCollection ? new(itemCollection.Count) : new();

do
{
result.Add(enumerator.Current);
} while (enumerator.MoveNext());

return result;
}

private class ProjectReference
{
public ProjectReference(string uniqueName, Array metadataElements, Array metadataValues)
Expand Down
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.Collections.Generic;
using Microsoft;
using Microsoft.VisualStudio.Shell;
using NuGet.ProjectManagement;
Expand All @@ -13,8 +14,10 @@ namespace NuGet.PackageManagement.VisualStudio
/// Since the base class <see cref="MSBuildNuGetProject"/> is in the NuGet.Core solution, it does not have
/// references to DTE.
/// </summary>
internal class VsMSBuildNuGetProject : MSBuildNuGetProject
public class VsMSBuildNuGetProject : MSBuildNuGetProject
{
private readonly IVsProjectAdapter _adapter;

public VsMSBuildNuGetProject(
IVsProjectAdapter projectAdapter,
IMSBuildProjectSystem msbuildNuGetProjectSystem,
Expand All @@ -31,11 +34,19 @@ public VsMSBuildNuGetProject(
Assumes.Present(msbuildNuGetProjectSystem);
Assumes.Present(projectServices);

_adapter = projectAdapter;

InternalMetadata.Add(NuGetProjectMetadataKeys.ProjectId, projectAdapter.ProjectId);
InternalMetadata.Add(ProjectBuildProperties.NuGetAudit, projectAdapter.BuildProperties.GetPropertyValue(ProjectBuildProperties.NuGetAudit));
InternalMetadata.Add(ProjectBuildProperties.NuGetAuditLevel, projectAdapter.BuildProperties.GetPropertyValue(ProjectBuildProperties.NuGetAuditLevel));

ProjectServices = projectServices;
}

public IReadOnlyList<(string id, string[] metadata)> GetItems(string itemName, params string[] metadataNames)
{
ThreadHelper.ThrowIfNotOnUIThread();
return VsManagedLanguagesProjectSystemServices.GetItems(_adapter, itemName, metadataNames);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,8 @@ await _logger.RunWithProgressAsync(
{
using SourceCacheContext sourceCacheContext = new();
List<SourceRepository> sourceRepositories = _sourceRepositoryProvider.GetRepositories().AsList();
Dictionary<string, RestoreAuditProperties> auditProperties = GetRestoreAuditProperties(allProjects);

Dictionary<string, RestoreAuditProperties> auditProperties = await GetRestoreAuditProperties(allProjects, token);

AuditChecker auditChecker = new(sourceRepositories, sourceCacheContext, _logger);
AuditCheckResult result = await auditChecker.CheckPackageVulnerabilitiesAsync(packages, auditProperties, token);
Expand Down Expand Up @@ -752,28 +753,50 @@ await _packageRestoreManager.RaisePackagesMissingEventForSolutionAsync(
token);
}

private static Dictionary<string, RestoreAuditProperties> GetRestoreAuditProperties(IEnumerable<NuGetProject> projects)
private static async Task<Dictionary<string, RestoreAuditProperties>> GetRestoreAuditProperties(IEnumerable<NuGetProject> projects, CancellationToken cancellationToken)
{
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

var restoreAuditProperties = new Dictionary<string, RestoreAuditProperties>(PathUtility.GetStringComparerBasedOnOS());

foreach (var nuGetProject in projects.NoAllocEnumerate())
{
if (nuGetProject.ProjectStyle == ProjectStyle.PackagesConfig)
{
var msbuildProject = (MSBuildNuGetProject)nuGetProject;
var msbuildProject = (VsMSBuildNuGetProject)nuGetProject;
var nuGetProjectName = (string)msbuildProject.GetMetadataOrNull(NuGetProjectMetadataKeys.Name);
var nugetAudit = (string)msbuildProject.GetMetadataOrNull(ProjectBuildProperties.NuGetAudit);
var auditLevel = (string)msbuildProject.GetMetadataOrNull(ProjectBuildProperties.NuGetAuditLevel);
var suppressions = GetSuppressions(msbuildProject);

var auditProperties = new RestoreAuditProperties()
{
EnableAudit = nugetAudit,
AuditLevel = auditLevel,
SuppressedAdvisories = suppressions,
};
restoreAuditProperties.Add(nuGetProjectName, auditProperties);
}
}

return restoreAuditProperties;

static HashSet<string> GetSuppressions(VsMSBuildNuGetProject msbuildProject)
{
var items = msbuildProject.GetItems(ProjectItems.NuGetAuditSuppress);
if (items?.Count > 0)
{
var suppressions = new HashSet<string>(items.Count, StringComparer.Ordinal);
for (int i = 0; i < items.Count; i++)
{
suppressions.Add(items[0].id);
}
return suppressions;
}

return null;
}
}

private void ValidatePackagesConfigLockFiles(IEnumerable<NuGetProject> allProjects, CancellationToken token)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,13 @@ private async Task<Dictionary<string, RestoreAuditProperties>> GetRestoreAuditPr
var nuGetProjectName = (string)msbuildProject.GetMetadataOrNull(NuGetProjectMetadataKeys.Name);
var nugetAudit = (string)msbuildProject.GetMetadataOrNull(ProjectBuildProperties.NuGetAudit);
var auditLevel = (string)msbuildProject.GetMetadataOrNull(ProjectBuildProperties.NuGetAuditLevel);
var suppressions = await GetSuppressionsAsync(msbuildProject);

var auditProperties = new RestoreAuditProperties()
{
EnableAudit = nugetAudit,
AuditLevel = auditLevel,
SuppressedAdvisories = suppressions
};
// Here be dragons.
// The key here, nuGetProjectName, needs to match the key in the dictionary in GetPackagesReferencesDictionaryAsync and all the constructors of PackageRestoreData.
Expand All @@ -214,6 +217,23 @@ private async Task<Dictionary<string, RestoreAuditProperties>> GetRestoreAuditPr
}

return restoreAuditProperties;

async Task<HashSet<string>> GetSuppressionsAsync(MSBuildNuGetProject msbuildProject)
{
var items = await msbuildProject.ProjectServices.ReferencesReader.GetItemsAsync(ProjectItems.NuGetAuditSuppress);
if (items?.Count > 0)
{
var suppressions = new HashSet<string>();
for (int i = 0; i < items.Count; i++)
{
(string url, _) = items[i];
suppressions.Add(url);
}
return suppressions;
}

return null;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Frameworks;
Expand Down Expand Up @@ -62,6 +61,12 @@ public Task<IEnumerable<ProjectRestoreReference>> GetProjectReferencesAsync(
return TaskResult.EmptyEnumerable<ProjectRestoreReference>();
}

public Task<IReadOnlyList<(string id, string[] metadata)>> GetItemsAsync(string itemTypeName, params string[] metadataNames)
{
IReadOnlyList<(string, string[])> items = Array.Empty<(string, string[])>();
return Task.FromResult(items);
}

public string GetPropertyValue(string propertyName)
{
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,13 @@ Task<IEnumerable<LibraryDependency>> GetPackageReferencesAsync(
Task<IEnumerable<ProjectRestoreReference>> GetProjectReferencesAsync(
Common.ILogger logger,
CancellationToken token);

/// <summary>
/// Returns a collection of items of the specified item type name.
/// </summary>
/// <param name="itemTypeName"></param>
/// <param name="metadataNames"></param>
/// <returns></returns>
Task<IReadOnlyList<(string id, string[] metadata)>> GetItemsAsync(string itemTypeName, params string[] metadataNames);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
NuGet.PackageManagement.AuditChecker.AuditChecker(System.Collections.Generic.List<NuGet.Protocol.Core.Types.SourceRepository!>! packageSources, System.Collections.Generic.IReadOnlyList<NuGet.Protocol.Core.Types.SourceRepository!>? auditSources, NuGet.Protocol.Core.Types.SourceCacheContext! sourceCacheContext, NuGet.Common.ILogger! logger) -> void
~NuGet.PackageManagement.PackageRestoreContext.AuditSources.get -> System.Collections.Generic.IReadOnlyList<NuGet.Protocol.Core.Types.SourceRepository>
~NuGet.PackageManagement.PackageRestoreContext.PackageRestoreContext(NuGet.PackageManagement.NuGetPackageManager nuGetPackageManager, System.Collections.Generic.IEnumerable<NuGet.PackageManagement.PackageRestoreData> packages, System.Threading.CancellationToken token, System.EventHandler<NuGet.PackageManagement.PackageRestoredEventArgs> packageRestoredEvent, System.EventHandler<NuGet.PackageManagement.PackageRestoreFailedEventArgs> packageRestoreFailedEvent, System.Collections.Generic.IEnumerable<NuGet.Protocol.Core.Types.SourceRepository> sourceRepositories, System.Collections.Generic.IReadOnlyList<NuGet.Protocol.Core.Types.SourceRepository> auditSources, int maxNumberOfParallelTasks, bool enableNuGetAudit, System.Collections.Generic.Dictionary<string, NuGet.ProjectModel.RestoreAuditProperties> restoreAuditProperties, NuGet.Common.ILogger logger) -> void
~NuGet.ProjectManagement.IProjectSystemReferencesReader.GetItemsAsync(string itemTypeName, params string[] metadataNames) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<(string id, string[] metadata)>>
Original file line number Diff line number Diff line change
Expand Up @@ -2016,6 +2016,11 @@ public Task<IEnumerable<ProjectRestoreReference>> GetProjectReferencesAsync(ILog

return Task.FromResult(projectRefs);
}

public Task<IReadOnlyList<(string id, string[] metadata)>> GetItemsAsync(string itemTypeName, params string[] metadataNames)
{
throw new NotImplementedException();
}
}

private class TestNonBuildIntegratedNuGetProject : NuGetProject, IDependencyGraphProject
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Test.Apex.VisualStudio.Shell;
using Microsoft.Test.Apex.VisualStudio.Solution;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NuGet.Protocol;
using NuGet.Test.Utility;
using NuGet.Versioning;
using Test.Utility;

namespace NuGet.Tests.Apex.NuGetEndToEndTests
{
[TestClass]
public class NuGetAuditTests : SharedVisualStudioHostTestClass
{
private const string TestPackageName = "Contoso.A";
private const string TestPackageVersionV1 = "1.0.0";
private const string TestPackageVersionV2 = "2.0.0";

[TestMethod]
[Timeout(DefaultTimeout)]
public async Task PackagesConfig_SuppressAdvisory()
{
// 1. Create Directory.Build.props with suppression for package.A cve1
// 2. Create mock server with package.A with cve1 and cve2
// 3. Add mock server to nuget.config
// 3. Create packages.config project
// 4. Install package.A
// 5. check error list to see if only cve2 is listed

// Arrange
SimpleTestPathContext testPathContext = new();
var dbpContents = @"<Project>
<ItemGroup>
<NuGetAuditSuppress Include=""https://cve.test/1"" />
</ItemGroup>
</Project>";
File.WriteAllText(Path.Combine(testPathContext.SolutionRoot, "Directory.Build.props"), dbpContents);

using var mockServer = new FileSystemBackedV3MockServer(testPathContext.PackageSource, sourceReportsVulnerabilities: true);
mockServer.Vulnerabilities.Add("contoso.a", new System.Collections.Generic.List<(Uri, PackageVulnerabilitySeverity, VersionRange)>
{
(new Uri("https://cve.test/1"), PackageVulnerabilitySeverity.High, VersionRange.Parse("(, 2.0.0)")),
(new Uri("https://cve.test/2"), PackageVulnerabilitySeverity.High, VersionRange.Parse("(, 2.0.0)")),
});

await CommonUtility.CreatePackageInSourceAsync(testPathContext.PackageSource, TestPackageName, TestPackageVersionV1);

mockServer.Start();

testPathContext.Settings.AddSource("auditSource", mockServer.ServiceIndexUri, allowInsecureConnectionsValue: "true");

EnsureVisualStudioHost();

using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger, addNetStandardFeeds: true, simpleTestPathContext: testPathContext);

var errorListService = VisualStudio.Get<ErrorListService>();
errorListService.ShowWarnings();

// Act
testContext.NuGetApexTestService.InstallPackage(testContext.Project.UniqueName, TestPackageName);
testContext.SolutionService.SaveAll();
testContext.SolutionService.Build();

// Assert
VisualStudio.AssertNoErrors();

var errors = VisualStudio.ObjectModel.Shell.ToolWindows.ErrorList.AllItems.Select(i => i.Description).ToList();
errors.Where(msg => msg.Contains(TestPackageName)).Should().ContainSingle();
errors.Single(msg => msg.Contains(TestPackageName)).Should().Contain("https://cve.test/2");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,5 +240,10 @@ public override Task<string> GetCacheFilePathAsync()
{
return base.GetCacheFilePathAsync();
}

public Task<IReadOnlyList<(string id, string[] metadata)>> GetItemsAsync(string itemTypeName, params string[] metadataNames)
{
throw new NotImplementedException();
}
}
}

0 comments on commit 8e96cf5

Please sign in to comment.