diff --git a/src/Microsoft.Sbom.Api/FormatValidator/IValidatedSBOM.cs b/src/Microsoft.Sbom.Api/FormatValidator/IValidatedSBOM.cs new file mode 100644 index 000000000..884a389ec --- /dev/null +++ b/src/Microsoft.Sbom.Api/FormatValidator/IValidatedSBOM.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Sbom.Api.FormatValidator; + +using System; +using System.Threading.Tasks; +using Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; + +public interface IValidatedSBOM: IDisposable +{ + public Task GetValidationResults(); + + public Task GetRawSPDXDocument(); +} diff --git a/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOM.cs b/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOM.cs index 30de6b933..ad6ce0f3e 100644 --- a/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOM.cs +++ b/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOM.cs @@ -13,7 +13,7 @@ namespace Microsoft.Sbom.Api.FormatValidator; using Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; using Microsoft.Sbom.Utils; -public class ValidatedSBOM +public class ValidatedSBOM: IValidatedSBOM { private readonly Stream sbomStream; private readonly int requiredSpdxMajorVersion = 2; @@ -50,6 +50,13 @@ public async Task GetRawSPDXDocument() return sbom; } + /// + public void Dispose() + { + this.sbomStream?.Dispose(); + GC.SuppressFinalize(this); + } + private async Task Initialize() { if (isInitialized) diff --git a/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOMFactory.cs b/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOMFactory.cs new file mode 100644 index 000000000..f09839efb --- /dev/null +++ b/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOMFactory.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Sbom.Api.FormatValidator; + +using System.IO; + +public class ValidatedSBOMFactory +{ + public virtual IValidatedSBOM CreateValidatedSBOM(string sbomFilePath) + { + var sbomStream = new StreamReader(sbomFilePath); + var validatedSbom = new ValidatedSBOM(sbomStream.BaseStream); + return validatedSbom; + } +} diff --git a/src/Microsoft.Sbom.Api/Workflows/Helpers/ISbomRedactor.cs b/src/Microsoft.Sbom.Api/Workflows/Helpers/ISbomRedactor.cs new file mode 100644 index 000000000..ad7f8b62c --- /dev/null +++ b/src/Microsoft.Sbom.Api/Workflows/Helpers/ISbomRedactor.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; +using Microsoft.Sbom.Api.FormatValidator; +using Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; + +namespace Microsoft.Sbom.Api.Workflows.Helpers; + +/// +/// SBOM redactor that removes file information from SBOMs +/// +public interface ISbomRedactor +{ + public Task RedactSBOMAsync(IValidatedSBOM sbom); +} diff --git a/src/Microsoft.Sbom.Api/Workflows/Helpers/SbomRedactor.cs b/src/Microsoft.Sbom.Api/Workflows/Helpers/SbomRedactor.cs new file mode 100644 index 000000000..21926266a --- /dev/null +++ b/src/Microsoft.Sbom.Api/Workflows/Helpers/SbomRedactor.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Sbom.Api.FormatValidator; +using Microsoft.Sbom.Common.Utils; +using Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; +using Serilog; + +namespace Microsoft.Sbom.Api.Workflows.Helpers; + +/// +/// SBOM redactor that removes file information from SBOMs +/// +public class SbomRedactor: ISbomRedactor +{ + private const string SpdxFileRelationshipPrefix = "SPDXRef-File-"; + + private readonly ILogger log; + + public SbomRedactor( + ILogger log) + { + this.log = log ?? throw new ArgumentNullException(nameof(log)); + } + + public virtual async Task RedactSBOMAsync(IValidatedSBOM sbom) + { + var spdx = await sbom.GetRawSPDXDocument(); + + if (spdx.Files != null) + { + this.log.Debug("Removing files section from SBOM."); + spdx.Files = null; + } + + RemovePackageFileRefs(spdx); + RemoveRelationshipsWithFileRefs(spdx); + UpdateDocumentNamespace(spdx); + + return spdx; + } + + private void RemovePackageFileRefs(FormatEnforcedSPDX2 spdx) + { + if (spdx.Packages != null) + { + foreach (var package in spdx.Packages) + { + if (package.HasFiles != null) + { + this.log.Debug($"Removing has files property from package {package.Name}."); + package.HasFiles = null; + } + + if (package.SourceInfo != null) + { + this.log.Debug($"Removing has sourceInfo property from package {package.Name}."); + package.SourceInfo = null; + } + } + } + } + + private void RemoveRelationshipsWithFileRefs(FormatEnforcedSPDX2 spdx) + { + if (spdx.Relationships != null) + { + var relationshipsToRemove = new List(); + foreach (var relationship in spdx.Relationships) + { + if (relationship.SourceElementId.Contains(SpdxFileRelationshipPrefix) || relationship.TargetElementId.Contains(SpdxFileRelationshipPrefix)) + { + relationshipsToRemove.Add(relationship); + } + } + + if (relationshipsToRemove.Any()) + { + this.log.Debug($"Removing {relationshipsToRemove.Count()} relationships with file references from SBOM."); + spdx.Relationships = spdx.Relationships.Except(relationshipsToRemove); + } + } + } + + private void UpdateDocumentNamespace(FormatEnforcedSPDX2 spdx) + { + if (!string.IsNullOrWhiteSpace(spdx.DocumentNamespace) && spdx.CreationInfo.Creators.Any(c => c.StartsWith("Tool: Microsoft.SBOMTool", StringComparison.OrdinalIgnoreCase))) + { + var existingNamespaceComponents = spdx.DocumentNamespace.Split('/'); + var uniqueComponent = IdentifierUtils.GetShortGuid(Guid.NewGuid()); + existingNamespaceComponents[^1] = uniqueComponent; + spdx.DocumentNamespace = string.Join("/", existingNamespaceComponents); + + this.log.Debug($"Updated document namespace to {spdx.DocumentNamespace}."); + } + } +} diff --git a/src/Microsoft.Sbom.Api/Workflows/SBOMRedactionWorkflow.cs b/src/Microsoft.Sbom.Api/Workflows/SBOMRedactionWorkflow.cs index ac9d416b9..680aa07ad 100644 --- a/src/Microsoft.Sbom.Api/Workflows/SBOMRedactionWorkflow.cs +++ b/src/Microsoft.Sbom.Api/Workflows/SBOMRedactionWorkflow.cs @@ -2,7 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Sbom.Api.FormatValidator; +using Microsoft.Sbom.Api.Workflows.Helpers; +using Microsoft.Sbom.Common; using Microsoft.Sbom.Common.Config; using Serilog; @@ -17,17 +24,121 @@ public class SbomRedactionWorkflow : IWorkflow private readonly IConfiguration configuration; + private readonly IFileSystemUtils fileSystemUtils; + + private readonly ValidatedSBOMFactory validatedSBOMFactory; + + private readonly ISbomRedactor sbomRedactor; + public SbomRedactionWorkflow( ILogger log, - IConfiguration configuration) + IConfiguration configuration, + IFileSystemUtils fileSystemUtils, + ValidatedSBOMFactory validatedSBOMFactory, + ISbomRedactor sbomRedactor) { this.log = log ?? throw new ArgumentNullException(nameof(log)); this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + this.fileSystemUtils = fileSystemUtils ?? throw new ArgumentNullException(nameof(fileSystemUtils)); + this.validatedSBOMFactory = validatedSBOMFactory ?? throw new ArgumentNullException(nameof(validatedSBOMFactory)); + this.sbomRedactor = sbomRedactor ?? throw new ArgumentNullException(nameof(sbomRedactor)); } public virtual async Task RunAsync() { - log.Information($"Running redaction for SBOM path {configuration.SbomPath?.Value} and SBOM dir {configuration.SbomDir?.Value}. Output dir: {configuration.OutputPath?.Value}"); - return await Task.FromResult(true); + ValidateDirStrucutre(); + var sbomPaths = GetInputSbomPaths(); + foreach (var sbomPath in sbomPaths) + { + IValidatedSBOM validatedSbom = null; + try + { + log.Information($"Validating SBOM {sbomPath}"); + validatedSbom = validatedSBOMFactory.CreateValidatedSBOM(sbomPath); + var validationDetails = await validatedSbom.GetValidationResults(); + if (validationDetails.Status != FormatValidationStatus.Valid) + { + throw new InvalidDataException($"Failed to validate {sbomPath}:\n{string.Join('\n', validationDetails.Errors)}"); + } + else + { + log.Information($"Redacting SBOM {sbomPath}"); + var outputPath = GetOutputPath(sbomPath); + var redactedSpdx = await this.sbomRedactor.RedactSBOMAsync(validatedSbom); + using (var outStream = fileSystemUtils.OpenWrite(outputPath)) + { + await JsonSerializer.SerializeAsync(outStream, redactedSpdx); + } + + log.Information($"Redacted SBOM {sbomPath} saved to {outputPath}"); + } + } + finally + { + validatedSbom?.Dispose(); + } + } + + return true; + } + + private string GetOutputPath(string sbomPath) + { + return fileSystemUtils.JoinPaths(configuration.OutputPath.Value, fileSystemUtils.GetFileName(sbomPath)); + } + + private IEnumerable GetInputSbomPaths() + { + if (configuration.SbomPath?.Value != null) + { + return new List() { configuration.SbomPath.Value }; + } + else if (configuration.SbomDir?.Value != null) + { + return fileSystemUtils.GetFilesInDirectory(configuration.SbomDir.Value); + } + else + { + throw new Exception("No valid input SBOMs to redact provided."); + } + } + + private string ValidateDirStrucutre() + { + string inputDir; + if (configuration.SbomDir?.Value != null && fileSystemUtils.DirectoryExists(configuration.SbomDir.Value)) + { + inputDir = configuration.SbomDir.Value; + } + else if (configuration.SbomPath?.Value != null && fileSystemUtils.FileExists(configuration.SbomPath.Value)) + { + inputDir = fileSystemUtils.GetDirectoryName(configuration.SbomPath.Value); + } + else + { + throw new ArgumentException("No valid input SBOMs to redact provided."); + } + + var outputDir = configuration.OutputPath.Value; + if (fileSystemUtils.GetFullPath(outputDir).Equals(fileSystemUtils.GetFullPath(inputDir))) + { + throw new ArgumentException("Output path cannot be the same as input SBOM directory."); + } + + if (!fileSystemUtils.DirectoryExists(outputDir)) + { + fileSystemUtils.CreateDirectory(outputDir); + } + + foreach (var sbom in GetInputSbomPaths()) + { + var outputPath = GetOutputPath(sbom); + if (fileSystemUtils.FileExists(outputPath)) + { + throw new ArgumentException($"Output file {outputPath} already exists. Please update and try again."); + } + } + + return outputDir; } } diff --git a/src/Microsoft.Sbom.Common/FileSystemUtils.cs b/src/Microsoft.Sbom.Common/FileSystemUtils.cs index 905b190d4..a4e874161 100644 --- a/src/Microsoft.Sbom.Common/FileSystemUtils.cs +++ b/src/Microsoft.Sbom.Common/FileSystemUtils.cs @@ -67,6 +67,9 @@ public string JoinPaths(string root, string relativePath, string secondRelativeP /// public bool FileExists(string path) => File.Exists(path); + /// + public string GetFileName(string filePath) => Path.GetFileName(filePath); + /// public Stream OpenWrite(string filePath) => new FileStream( filePath, diff --git a/src/Microsoft.Sbom.Common/IFileSystemUtils.cs b/src/Microsoft.Sbom.Common/IFileSystemUtils.cs index 8ddc2b83f..f60b49c8f 100644 --- a/src/Microsoft.Sbom.Common/IFileSystemUtils.cs +++ b/src/Microsoft.Sbom.Common/IFileSystemUtils.cs @@ -104,6 +104,13 @@ public interface IFileSystemUtils /// True if the file exists, false otherwise. bool FileExists(string path); + /// + /// Get the file name of a file. + /// + /// The absolute path of the file. + /// The file name. + string GetFileName(string filePath); + /// /// Get the directory name of a file. /// diff --git a/src/Microsoft.Sbom.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Microsoft.Sbom.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 7a0fc6a43..eea654e24 100644 --- a/src/Microsoft.Sbom.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Microsoft.Sbom.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -15,6 +15,7 @@ using Microsoft.Sbom.Api.Entities.Output; using Microsoft.Sbom.Api.Executors; using Microsoft.Sbom.Api.Filters; +using Microsoft.Sbom.Api.FormatValidator; using Microsoft.Sbom.Api.Hashing; using Microsoft.Sbom.Api.Manifest; using Microsoft.Sbom.Api.Manifest.Configuration; @@ -73,6 +74,8 @@ public static IServiceCollection AddSbomTool(this IServiceCollection services, L .AddTransient, SbomParserBasedValidationWorkflow>() .AddTransient, SbomGenerationWorkflow>() .AddTransient, SbomRedactionWorkflow>() + .AddTransient() + .AddTransient() .AddTransient() .AddTransient, DownloadedRootPathFilter>() .AddTransient, ManifestFolderFilter>() diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/CreationInfo.cs b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/CreationInfo.cs index 7de88ab3b..cd2dc36b3 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/CreationInfo.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/CreationInfo.cs @@ -11,6 +11,7 @@ namespace Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; /// public class CreationInfo { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("comment")] public string Comment { get; set; } @@ -27,6 +28,7 @@ public class CreationInfo [JsonPropertyName("creators")] public IEnumerable Creators { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("licenseListVersion")] public string LicenseListVersion { get; set; } } diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/FormatEnforcedSPDX2.cs b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/FormatEnforcedSPDX2.cs index 5e3e7cd9b..3d6930e4b 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/FormatEnforcedSPDX2.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/FormatEnforcedSPDX2.cs @@ -10,12 +10,14 @@ public class FormatEnforcedSPDX2 : SPDX2RequiredProperties { // These attributes are not required by the SPDX spec, but may be present in // SBOMs produced by sbom-tool or 3P SBOMs. We want to (de)serialize them if they are present. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("comment")] public string Comment { get; set; } [JsonPropertyName("documentDescribes")] public IEnumerable DocumentDescribes { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("files")] public IEnumerable Files { get; set; } diff --git a/test/Microsoft.Sbom.Api.Tests/Workflows/Helpers/SbomRedactorTests.cs b/test/Microsoft.Sbom.Api.Tests/Workflows/Helpers/SbomRedactorTests.cs new file mode 100644 index 000000000..2bd0e6ee2 --- /dev/null +++ b/test/Microsoft.Sbom.Api.Tests/Workflows/Helpers/SbomRedactorTests.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Sbom.Api.FormatValidator; +using Microsoft.Sbom.Api.Workflows.Helpers; +using Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Serilog; + +namespace Microsoft.Sbom.Api.Tests.Workflows.Helpers; + +[TestClass] +public class SbomRedactorTests +{ + private Mock mockLogger; + private Mock mockValidatedSbom; + + private SbomRedactor testSubject; + + [TestInitialize] + public void Init() + { + mockLogger = new Mock(); + mockValidatedSbom = new Mock(); + testSubject = new SbomRedactor(mockLogger.Object); + } + + [TestCleanup] + public void Reset() + { + mockLogger.VerifyAll(); + } + + [TestMethod] + public async Task SbomRedactor_RemovesFilesSection() + { + var mockSbom = new FormatEnforcedSPDX2 + { + Files = new List + { + new SPDXFile() + } + }; + mockValidatedSbom.Setup(x => x.GetRawSPDXDocument()).ReturnsAsync(mockSbom); + await testSubject.RedactSBOMAsync(mockValidatedSbom.Object); + Assert.IsNull(mockSbom.Files); + } + + [TestMethod] + public async Task SbomRedactor_RemovesPackageFileRefs() + { + var mockSbom = new FormatEnforcedSPDX2 + { + Packages = new List + { + new SPDXPackage() + { + SpdxId = "package-1", + HasFiles = new List + { + "file-1", + "file-2", + } + }, + new SPDXPackage() + { + SpdxId = "package-2", + SourceInfo = "source-info" + }, + new SPDXPackage() + { + SpdxId = "package-3", + } + } + }; + mockValidatedSbom.Setup(x => x.GetRawSPDXDocument()).ReturnsAsync(mockSbom); + await testSubject.RedactSBOMAsync(mockValidatedSbom.Object); + Assert.AreEqual(mockSbom.Packages.Count(), 3); + foreach (var package in mockSbom.Packages) + { + Assert.IsNull(package.HasFiles); + Assert.IsNull(package.SourceInfo); + Assert.IsNotNull(package.SpdxId); + } + } + + [TestMethod] + public async Task SbomRedactor_RemovesRelationshipsWithFileRefs() + { + var unredactedRelationship = new SPDXRelationship() + { + SourceElementId = "source", + TargetElementId = "target", + RelationshipType = "relationship-3", + }; + var mockSbom = new FormatEnforcedSPDX2 + { + Relationships = new List + { + new SPDXRelationship() + { + SourceElementId = "SPDXRef-File-1", + TargetElementId = "target", + RelationshipType = "relationship-1", + }, + new SPDXRelationship() + { + SourceElementId = "source", + TargetElementId = "SPDXRef-File-2", + RelationshipType = "relationship-2", + }, + unredactedRelationship + } + }; + mockValidatedSbom.Setup(x => x.GetRawSPDXDocument()).ReturnsAsync(mockSbom); + await testSubject.RedactSBOMAsync(mockValidatedSbom.Object); + Assert.AreEqual(mockSbom.Relationships.Count(), 1); + Assert.AreEqual(mockSbom.Relationships.First(), unredactedRelationship); + } + + [TestMethod] + public async Task SbomRedactor_UpdatesDocNamespaceForMsftSboms() + { + var docNamespace = "microsoft/test/namespace/fakeguid"; + var mockSbom = new FormatEnforcedSPDX2 + { + CreationInfo = new CreationInfo + { + Creators = new List { "Tool: Microsoft.SBOMTool" } + }, + DocumentNamespace = docNamespace + }; + mockValidatedSbom.Setup(x => x.GetRawSPDXDocument()).ReturnsAsync(mockSbom); + await testSubject.RedactSBOMAsync(mockValidatedSbom.Object); + Assert.IsTrue(mockSbom.DocumentNamespace.Contains("microsoft/test/namespace/")); + Assert.IsFalse(mockSbom.DocumentNamespace.Contains("fakeguid")); + } + + [TestMethod] + public async Task SbomRedactor_DoesNotEditDocNamespaceForNonMsftSboms() + { + var docNamespace = "test-namespace"; + var mockSbom = new FormatEnforcedSPDX2 + { + CreationInfo = new CreationInfo + { + Creators = new List { "non-msft-tool" } + }, + DocumentNamespace = docNamespace + }; + mockValidatedSbom.Setup(x => x.GetRawSPDXDocument()).ReturnsAsync(mockSbom); + await testSubject.RedactSBOMAsync(mockValidatedSbom.Object); + Assert.AreEqual(mockSbom.DocumentNamespace, docNamespace); + } +} diff --git a/test/Microsoft.Sbom.Api.Tests/Workflows/SbomRedactionWorkflowTests.cs b/test/Microsoft.Sbom.Api.Tests/Workflows/SbomRedactionWorkflowTests.cs index 51a22a017..78f2d5b9e 100644 --- a/test/Microsoft.Sbom.Api.Tests/Workflows/SbomRedactionWorkflowTests.cs +++ b/test/Microsoft.Sbom.Api.Tests/Workflows/SbomRedactionWorkflowTests.cs @@ -3,11 +3,19 @@ #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +using System; +using System.IO; +using System.Text; using System.Threading.Tasks; +using Microsoft.Sbom.Api.FormatValidator; using Microsoft.Sbom.Api.Workflows; +using Microsoft.Sbom.Api.Workflows.Helpers; +using Microsoft.Sbom.Common; using Microsoft.Sbom.Common.Config; +using Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using PowerArgs; using Serilog; namespace Microsoft.Sbom.Workflows; @@ -19,33 +27,137 @@ public class SbomRedactionWorkflowTests { private Mock mockLogger; private Mock configurationMock; + private Mock fileSystemUtilsMock; + private Mock validatedSBOMFactoryMock; + private Mock sbomRedactorMock; private SbomRedactionWorkflow testSubject; + private const string SbomPathStub = "sbom-path"; + private const string SbomDirStub = "sbom-dir"; + private const string OutDirStub = "out-dir"; + private const string OutPathStub = "out-path"; + private const string SbomFileNameStub = "sbom-name"; + [TestInitialize] public void Init() { mockLogger = new Mock(); configurationMock = new Mock(); + fileSystemUtilsMock = new Mock(); + validatedSBOMFactoryMock = new Mock(); + sbomRedactorMock = new Mock(); testSubject = new SbomRedactionWorkflow( mockLogger.Object, - configurationMock.Object); + configurationMock.Object, + fileSystemUtilsMock.Object, + validatedSBOMFactoryMock.Object, + sbomRedactorMock.Object); } [TestCleanup] public void Reset() { mockLogger.VerifyAll(); + fileSystemUtilsMock.VerifyAll(); configurationMock.VerifyAll(); + validatedSBOMFactoryMock.VerifyAll(); + sbomRedactorMock.VerifyAll(); } [TestMethod] - public async Task SbomRedactionTests_Succeeds() + [ExpectedException(typeof(ArgumentException))] + public async Task SbomRedactionWorkflow_FailsOnNoSbomsProvided() { - mockLogger.Setup(x => x.Information($"Running redaction for SBOM path path and SBOM dir dir. Output dir: out")); - configurationMock.SetupGet(c => c.SbomPath).Returns(new ConfigurationSetting { Value = "path" }); - configurationMock.SetupGet(c => c.SbomDir).Returns(new ConfigurationSetting { Value = "dir" }); - configurationMock.SetupGet(c => c.OutputPath).Returns(new ConfigurationSetting { Value = "out" }); + var result = await testSubject.RunAsync(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public async Task SbomRedactionWorkflow_FailsOnMatchingInputOutputDirs() + { + configurationMock.SetupGet(c => c.SbomDir).Returns(new ConfigurationSetting { Value = SbomDirStub }); + configurationMock.SetupGet(c => c.OutputPath).Returns(new ConfigurationSetting { Value = SbomDirStub }); + fileSystemUtilsMock.Setup(m => m.DirectoryExists(SbomDirStub)).Returns(true).Verifiable(); + fileSystemUtilsMock.Setup(m => m.GetFullPath(SbomDirStub)).Returns(SbomDirStub).Verifiable(); + var result = await testSubject.RunAsync(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public async Task SbomRedactionWorkflow_FailsOnExistingOutputSbom() + { + configurationMock.SetupGet(c => c.SbomPath).Returns(new ConfigurationSetting { Value = SbomPathStub }); + configurationMock.SetupGet(c => c.OutputPath).Returns(new ConfigurationSetting { Value = OutDirStub }); + fileSystemUtilsMock.Setup(m => m.FileExists(SbomPathStub)).Returns(true).Verifiable(); + fileSystemUtilsMock.Setup(m => m.GetDirectoryName(SbomPathStub)).Returns(SbomDirStub).Verifiable(); + fileSystemUtilsMock.Setup(m => m.DirectoryExists(OutDirStub)).Returns(true).Verifiable(); + fileSystemUtilsMock.Setup(m => m.GetFullPath(SbomDirStub)).Returns(SbomDirStub).Verifiable(); + fileSystemUtilsMock.Setup(m => m.GetFullPath(OutDirStub)).Returns(OutDirStub).Verifiable(); + + // GetOutputPath + fileSystemUtilsMock.Setup(m => m.GetFileName(SbomPathStub)).Returns(SbomFileNameStub).Verifiable(); + fileSystemUtilsMock.Setup(m => m.JoinPaths(OutDirStub, SbomFileNameStub)).Returns(OutPathStub).Verifiable(); + + // Output already file exists + fileSystemUtilsMock.Setup(m => m.FileExists(OutPathStub)).Returns(true).Verifiable(); + + var result = await testSubject.RunAsync(); + } + + [TestMethod] + [ExpectedException(typeof(InvalidDataException))] + public async Task SbomRedactionWorkflow_FailsOnInvalidSboms() + { + SetUpDirStructure(); + + fileSystemUtilsMock.Setup(m => m.GetFilesInDirectory(SbomDirStub, true)).Returns(new string[] { SbomPathStub }).Verifiable(); + var validatedSbomMock = new Mock(); + validatedSBOMFactoryMock.Setup(m => m.CreateValidatedSBOM(SbomPathStub)).Returns(validatedSbomMock.Object).Verifiable(); + var validationRes = new FormatValidationResults(); + validationRes.AggregateValidationStatus(FormatValidationStatus.NotValid); + validatedSbomMock.Setup(m => m.GetValidationResults()).ReturnsAsync(validationRes).Verifiable(); + validatedSbomMock.Setup(m => m.Dispose()).Verifiable(); + + var result = await testSubject.RunAsync(); + } + + [TestMethod] + public async Task SbomRedactionWorkflow_RunsRedactionOnValidSboms() + { + SetUpDirStructure(); + + fileSystemUtilsMock.Setup(m => m.GetFilesInDirectory(SbomDirStub, true)).Returns(new string[] { SbomPathStub }).Verifiable(); + var validatedSbomMock = new Mock(); + validatedSBOMFactoryMock.Setup(m => m.CreateValidatedSBOM(SbomPathStub)).Returns(validatedSbomMock.Object).Verifiable(); + var validationRes = new FormatValidationResults(); + validationRes.AggregateValidationStatus(FormatValidationStatus.Valid); + validatedSbomMock.Setup(m => m.GetValidationResults()).ReturnsAsync(validationRes).Verifiable(); + var redactedContent = new FormatEnforcedSPDX2() { Name = "redacted" }; + sbomRedactorMock.Setup(m => m.RedactSBOMAsync(validatedSbomMock.Object)).ReturnsAsync(redactedContent).Verifiable(); + var outStream = new MemoryStream(); + fileSystemUtilsMock.Setup(m => m.OpenWrite(OutPathStub)).Returns(outStream).Verifiable(); + validatedSbomMock.Setup(m => m.Dispose()).Verifiable(); + var result = await testSubject.RunAsync(); Assert.IsTrue(result); + var redactedResult = Encoding.ASCII.GetString(outStream.ToArray()); + Assert.IsTrue(redactedResult.Contains(@"""name"":""redacted""")); + } + + private void SetUpDirStructure() + { + configurationMock.SetupGet(c => c.SbomDir).Returns(new ConfigurationSetting { Value = SbomDirStub }); + configurationMock.SetupGet(c => c.OutputPath).Returns(new ConfigurationSetting { Value = OutDirStub }); + fileSystemUtilsMock.Setup(m => m.DirectoryExists(SbomDirStub)).Returns(true).Verifiable(); + fileSystemUtilsMock.Setup(m => m.DirectoryExists(OutDirStub)).Returns(true).Verifiable(); + fileSystemUtilsMock.Setup(m => m.GetFullPath(SbomDirStub)).Returns(SbomDirStub).Verifiable(); + fileSystemUtilsMock.Setup(m => m.GetFullPath(OutDirStub)).Returns(OutDirStub).Verifiable(); + + // GetOutputPath + fileSystemUtilsMock.Setup(m => m.GetFileName(SbomPathStub)).Returns(SbomFileNameStub).Verifiable(); + fileSystemUtilsMock.Setup(m => m.JoinPaths(OutDirStub, SbomFileNameStub)).Returns(OutPathStub).Verifiable(); + + // Output already file exists + fileSystemUtilsMock.Setup(m => m.FileExists(OutPathStub)).Returns(false).Verifiable(); } }