diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/Package/IVulnerable.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/Package/IVulnerable.cs new file mode 100644 index 00000000000..f634aa55880 --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/Package/IVulnerable.cs @@ -0,0 +1,20 @@ +// 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.Collections.Generic; +using NuGet.Protocol; +using NuGet.VisualStudio.Internal.Contracts; + +namespace NuGet.PackageManagement.UI +{ + interface IVulnerable + { + public IEnumerable Vulnerabilities { get; } + + public bool IsVulnerable { get; } + + public PackageVulnerabilitySeverity VulnerabilityMaxSeverity { get; } + } +} diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/Package/VulnerableCapability.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/Package/VulnerableCapability.cs new file mode 100644 index 00000000000..d857b8351fe --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/Package/VulnerableCapability.cs @@ -0,0 +1,48 @@ +// 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.Collections.Generic; +using System.Linq; +using NuGet.Protocol; +using NuGet.VisualStudio.Internal.Contracts; + +namespace NuGet.PackageManagement.UI +{ + public class VulnerableCapability : IVulnerable + { + private IEnumerable _vulnerabilities = []; + + public IEnumerable Vulnerabilities + { + get => _vulnerabilities; + private set => _vulnerabilities = value.OrderByDescending(v => v?.Severity ?? -1); + } + + public bool IsVulnerable => Vulnerabilities.Any(v => v != null); + + public PackageVulnerabilitySeverity VulnerabilityMaxSeverity + { + get + { + // Vulnerabilities are ordered so the first element is always the highest severity + int? severity = Vulnerabilities.FirstOrDefault()?.Severity; + if (severity != null && Enum.IsDefined(typeof(PackageVulnerabilitySeverity), severity)) + { + return (PackageVulnerabilitySeverity)severity; + } + else + { + return PackageVulnerabilitySeverity.Unknown; + } + } + } + + public VulnerableCapability(IEnumerable vulnerabilities) + { + Vulnerabilities = vulnerabilities ?? throw new ArgumentNullException(nameof(vulnerabilities)); + } + } +} diff --git a/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/Models/Package/VulnerableCapabilityTests.cs b/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/Models/Package/VulnerableCapabilityTests.cs new file mode 100644 index 00000000000..336575090fc --- /dev/null +++ b/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/Models/Package/VulnerableCapabilityTests.cs @@ -0,0 +1,92 @@ +// 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.Collections.Generic; +using System.Linq; +using NuGet.Protocol; +using NuGet.VisualStudio.Internal.Contracts; +using Xunit; + +namespace NuGet.PackageManagement.UI.Test +{ + public class VulnerableCapabilityTests + { + [Theory] + [InlineData(1, true)] + [InlineData(0, false)] + public void IsVulnerable_VariousVulnerabilities_ReturnsExpected(int severity, bool expected) + { + // Arrange + IEnumerable vulnerabilities = severity > 0 + ? [new(new Uri("http://example.com"), severity)] + : []; + var vulnerableCapability = new VulnerableCapability(vulnerabilities); + + // Act + var result = vulnerableCapability.IsVulnerable; + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void IsVulnerable_NullVulnerabilityInCollection_ReturnsFalse() + { + // Arrange + List vulnerabilities = [null]; + var vulnerableCapability = new VulnerableCapability(vulnerabilities); + + // Act + var result = vulnerableCapability.IsVulnerable; + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData(new int[] { (int)PackageVulnerabilitySeverity.Low, (int)PackageVulnerabilitySeverity.High }, PackageVulnerabilitySeverity.High)] + [InlineData(new int[] { (int)PackageVulnerabilitySeverity.Critical, (int)PackageVulnerabilitySeverity.Unknown }, PackageVulnerabilitySeverity.Critical)] + [InlineData(new int[] { }, PackageVulnerabilitySeverity.Unknown)] + [InlineData(new int[] { -2 }, PackageVulnerabilitySeverity.Unknown)] + public void VulnerabilityMaxSeverity_VariousVulnerabilities_ReturnsExpected(int[] severities, PackageVulnerabilitySeverity expected) + { + // Arrange + var vulnerabilities = severities.Select(severity => new PackageVulnerabilityMetadataContextInfo(new Uri("http://example.com"), severity)).ToList(); + var vulnerableCapability = new VulnerableCapability(vulnerabilities); + + // Act + var result = vulnerableCapability.VulnerabilityMaxSeverity; + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void Constructor_OrdersVulnerabilitiesBySeverity_DescendingOrder() + { + // Arrange + var vulnerabilities = new List + { + new(new Uri("http://example.com/high"), (int)PackageVulnerabilitySeverity.High), + new(new Uri("http://example.com/low"), (int)PackageVulnerabilitySeverity.Low), + new(new Uri("http://example.com/moderate"), (int)PackageVulnerabilitySeverity.Moderate), + new(new Uri("http://example.com/critical"), (int)PackageVulnerabilitySeverity.Critical) + }; + + // Act + var vulnerableCapability = new VulnerableCapability(vulnerabilities); + var orderedVulnerabilities = vulnerableCapability.Vulnerabilities.ToList(); + + // Assert + Assert.Collection(orderedVulnerabilities, + item => Assert.Equal((int)PackageVulnerabilitySeverity.Critical, item.Severity), + item => Assert.Equal((int)PackageVulnerabilitySeverity.High, item.Severity), + item => Assert.Equal((int)PackageVulnerabilitySeverity.Moderate, item.Severity), + item => Assert.Equal((int)PackageVulnerabilitySeverity.Low, item.Severity) + ); + } + } +}