From 293fc608ed7711cb47e4f843faf78ffb79b9fa05 Mon Sep 17 00:00:00 2001 From: Sebastian Gomez <69322674+sebasgomez238@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:53:19 -0500 Subject: [PATCH] Add Author/License to LinuxComponent (#934) * Add Author/License to LinuxComponent * Add unit test. Remove comment * Increase coverage. * Feedback * Fix. Only Author and License are nullable. --------- Co-authored-by: Sebastian Gomez --- .../TypedComponent/LinuxComponent.cs | 12 ++- .../linux/LinuxScanner.cs | 36 +++++++- .../LinuxScannerTests.cs | 86 +++++++++++++++++-- 3 files changed, 126 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs index a37f56ed1..89c1a8e23 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs @@ -1,4 +1,4 @@ -namespace Microsoft.ComponentDetection.Contracts.TypedComponent; +namespace Microsoft.ComponentDetection.Contracts.TypedComponent; using PackageUrl; @@ -9,12 +9,14 @@ private LinuxComponent() /* Reserved for deserialization */ } - public LinuxComponent(string distribution, string release, string name, string version) + public LinuxComponent(string distribution, string release, string name, string version, string license = null, string author = null) { this.Distribution = this.ValidateRequiredInput(distribution, nameof(this.Distribution), nameof(ComponentType.Linux)); this.Release = this.ValidateRequiredInput(release, nameof(this.Release), nameof(ComponentType.Linux)); this.Name = this.ValidateRequiredInput(name, nameof(this.Name), nameof(ComponentType.Linux)); this.Version = this.ValidateRequiredInput(version, nameof(this.Version), nameof(ComponentType.Linux)); + this.License = license; + this.Author = author; } public string Distribution { get; set; } @@ -25,6 +27,12 @@ public LinuxComponent(string distribution, string release, string name, string v public string Version { get; set; } +#nullable enable + public string? License { get; set; } + + public string? Author { get; set; } +#nullable disable + public override ComponentType Type => ComponentType.Linux; public override string Id => $"{this.Distribution} {this.Release} {this.Name} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs index b83678472..0908c79fc 100644 --- a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs +++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs @@ -104,7 +104,7 @@ public async Task> ScanLinuxAsync(string .DistinctBy(artifact => (artifact.Name, artifact.Version)) .Where(artifact => AllowedArtifactTypes.Contains(artifact.Type)) .Select(artifact => - (Component: new LinuxComponent(syftOutput.Distro.Id, syftOutput.Distro.VersionId, artifact.Name, artifact.Version), layerIds: artifact.Locations.Select(location => location.LayerId).Distinct())); + (Component: new LinuxComponent(syftOutput.Distro.Id, syftOutput.Distro.VersionId, artifact.Name, artifact.Version, this.GetLicenseFromArtifactElement(artifact), this.GetSupplierFromArtifactElement(artifact)), layerIds: artifact.Locations.Select(location => location.LayerId).Distinct())); foreach (var (component, layers) in linuxComponentsWithLayers) { @@ -137,6 +137,40 @@ public async Task> ScanLinuxAsync(string } } + private string GetSupplierFromArtifactElement(ArtifactElement artifact) + { + var supplier = artifact.Metadata?.Author; + if (!string.IsNullOrEmpty(supplier)) + { + return supplier; + } + + supplier = artifact.Metadata?.Maintainer; + if (!string.IsNullOrEmpty(supplier)) + { + return supplier; + } + + return null; + } + + private string GetLicenseFromArtifactElement(ArtifactElement artifact) + { + var license = artifact.Metadata?.License?.String; + if (license != null) + { + return license.ToString(); + } + + var licenses = artifact.Licenses; + if (licenses != null && licenses.Any()) + { + return string.Join(", ", licenses.Select(l => l.ToString())); + } + + return null; + } + internal sealed class LinuxComponentRecord { public string Name { get; set; } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs index e90d7e544..c6a3664eb 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs @@ -17,7 +17,7 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; [TestCategory("Governance/ComponentDetection")] public class LinuxScannerTests { - private const string SyftOutput = @"{ + private const string SyftOutputLicensesFieldAndAuthor = @"{ ""distro"": { ""id"":""test-distribution"", ""versionId"":""1.0.0"" @@ -32,7 +32,59 @@ public class LinuxScannerTests ""path"": ""/var/lib/dpkg/status"", ""layerID"": ""sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971"" } - ] + ], + ""metadata"": { + ""author"": ""John Doe"" + }, + ""licenses"": [ + ""MIT"", + ""GPLv2"", + ""GPLv3"" + ] + } + ] + }"; + + private const string SyftOutputLicenseFieldAndMaintainer = @"{ + ""distro"": { + ""id"":""test-distribution"", + ""versionId"":""1.0.0"" + }, + ""artifacts"": [ + { + ""name"":""test"", + ""version"":""1.0.0"", + ""type"":""deb"", + ""locations"": [ + { + ""path"": ""/var/lib/dpkg/status"", + ""layerID"": ""sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971"" + } + ], + ""metadata"": { + ""maintainer"": ""John Doe"", + ""license"": ""MIT, GPLv2, GPLv3"" + } + } + ] + }"; + + private const string SyftOutputNoAuthorOrLicense = @"{ + ""distro"": { + ""id"":""test-distribution"", + ""versionId"":""1.0.0"" + }, + ""artifacts"": [ + { + ""name"":""test"", + ""version"":""1.0.0"", + ""type"":""deb"", + ""locations"": [ + { + ""path"": ""/var/lib/dpkg/status"", + ""layerID"": ""sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971"" + } + ], } ] }"; @@ -47,8 +99,6 @@ public LinuxScannerTests() this.mockDockerService.Setup(service => service.CanPingDockerAsync(It.IsAny())) .ReturnsAsync(true); this.mockDockerService.Setup(service => service.TryPullImageAsync(It.IsAny(), It.IsAny())); - this.mockDockerService.Setup(service => service.CreateAndRunContainerAsync(It.IsAny(), It.IsAny>(), It.IsAny())) - .ReturnsAsync((SyftOutput, string.Empty)); this.mockLogger = new Mock>(); @@ -56,8 +106,32 @@ public LinuxScannerTests() } [TestMethod] - public async Task TestLinuxScannerAsync() + [DataRow(SyftOutputLicensesFieldAndAuthor)] + [DataRow(SyftOutputLicenseFieldAndMaintainer)] + public async Task TestLinuxScannerAsync(string syftOutput) + { + this.mockDockerService.Setup(service => service.CreateAndRunContainerAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync((syftOutput, string.Empty)); + + var result = (await this.linuxScanner.ScanLinuxAsync("fake_hash", new[] { new DockerLayer { LayerIndex = 0, DiffId = "sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971" } }, 0)).First().LinuxComponents; + + result.Should().ContainSingle(); + var package = result.First(); + package.Name.Should().Be("test"); + package.Version.Should().Be("1.0.0"); + package.Release.Should().Be("1.0.0"); + package.Distribution.Should().Be("test-distribution"); + package.Author.Should().Be("John Doe"); + package.License.Should().Be("MIT, GPLv2, GPLv3"); + } + + [TestMethod] + [DataRow(SyftOutputNoAuthorOrLicense)] + public async Task TestLinuxScanner_ReturnsNullAuthorAndLicense_Async(string syftOutput) { + this.mockDockerService.Setup(service => service.CreateAndRunContainerAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync((syftOutput, string.Empty)); + var result = (await this.linuxScanner.ScanLinuxAsync("fake_hash", new[] { new DockerLayer { LayerIndex = 0, DiffId = "sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971" } }, 0)).First().LinuxComponents; result.Should().ContainSingle(); @@ -66,5 +140,7 @@ public async Task TestLinuxScannerAsync() package.Version.Should().Be("1.0.0"); package.Release.Should().Be("1.0.0"); package.Distribution.Should().Be("test-distribution"); + package.Author.Should().Be(null); + package.License.Should().Be(null); } }