diff --git a/src/Microsoft.Sbom.Api/FormatValidator/IValidatedSBOM.cs b/src/Microsoft.Sbom.Api/FormatValidator/IValidatedSBOM.cs new file mode 100644 index 00000000..ef1d5dbe --- /dev/null +++ b/src/Microsoft.Sbom.Api/FormatValidator/IValidatedSBOM.cs @@ -0,0 +1,14 @@ +// 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.Threading.Tasks; +using Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; + +public interface IValidatedSBOM +{ + 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 30de6b93..8e03e3e0 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; diff --git a/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOMFactory.cs b/src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOMFactory.cs new file mode 100644 index 00000000..f09839ef --- /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/SbomRedactor.cs b/src/Microsoft.Sbom.Api/Workflows/Helpers/SbomRedactor.cs new file mode 100644 index 00000000..bc419334 --- /dev/null +++ b/src/Microsoft.Sbom.Api/Workflows/Helpers/SbomRedactor.cs @@ -0,0 +1,88 @@ +// 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; + +namespace Microsoft.Sbom.Api.Workflows.Helpers; + +/// +/// SBOM redactor that removes file information from SBOMs +/// +public class SbomRedactor +{ + private const string SpdxFileRelationshipPrefix = "SPDXRef-File-"; + + public virtual async Task RedactSBOMAsync(IValidatedSBOM sbom) + { + var spdx = await sbom.GetRawSPDXDocument(); + + if (spdx.Files != null) + { + 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) + { + package.HasFiles = null; + } + + if (package.SourceInfo != null) + { + 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()) + { + spdx.Relationships = spdx.Relationships.Except(relationshipsToRemove); + } + } + } + + private void UpdateDocumentNamespace(FormatEnforcedSPDX2 spdx) + { + if (string.IsNullOrWhiteSpace(spdx.DocumentNamespace) || !spdx.DocumentNamespace.Contains("microsoft")) + { + return; + } + + var existingNamespaceComponents = spdx.DocumentNamespace.Split('/'); + var uniqueComponent = IdentifierUtils.GetShortGuid(Guid.NewGuid()); + existingNamespaceComponents[^1] = uniqueComponent; + spdx.DocumentNamespace = string.Join("/", existingNamespaceComponents); + } +} diff --git a/src/Microsoft.Sbom.Api/Workflows/SBOMRedactionWorkflow.cs b/src/Microsoft.Sbom.Api/Workflows/SBOMRedactionWorkflow.cs index ac9d416b..22a2a829 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,133 @@ public class SbomRedactionWorkflow : IWorkflow private readonly IConfiguration configuration; + private readonly IFileSystemUtils fileSystemUtils; + + private readonly ValidatedSBOMFactory validatedSBOMFactory; + + private readonly SbomRedactor sbomRedactor; + public SbomRedactionWorkflow( ILogger log, - IConfiguration configuration) + IConfiguration configuration, + IFileSystemUtils fileSystemUtils, + ValidatedSBOMFactory validatedSBOMFactory = null, + SbomRedactor sbomRedactor = null) { 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 ?? new ValidatedSBOMFactory(); + this.sbomRedactor = sbomRedactor ?? new 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 sboms = await GetValidSbomsAsync(); + + log.Information($"Running redaction on the following SBOMs: {string.Join(' ', sboms.Select(sbom => sbom.Key))}"); + foreach (var (sbomPath, validatedSbom) in sboms) + { + try + { + 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}"); + } + catch (Exception ex) + { + throw new Exception($"Failed to redact {sbomPath}: {ex.Message}", ex); + } + } + + return true; + } + + private async Task> GetValidSbomsAsync() + { + var sbomPaths = GetInputSbomPaths(); + var validatedSboms = new Dictionary(); + foreach (var sbom in sbomPaths) + { + log.Information($"Validating SBOM {sbom}"); + var validatedSbom = validatedSBOMFactory.CreateValidatedSBOM(sbom); + var validationDetails = await validatedSbom.GetValidationResults(); + if (validationDetails.Status != FormatValidationStatus.Valid) + { + throw new InvalidDataException($"Failed to validate {sbom}:\n{string.Join('\n', validationDetails.Errors)}"); + } + else + { + validatedSboms.Add(sbom, validatedSbom); + } + } + + return validatedSboms; + } + + 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 905b190d..a4e87416 100644 --- a/src/Microsoft.Sbom.Common/FileSystemUtils.cs +++ b/src/Microsoft.Sbom.Common/FileSystemUtils.cs @@ -67,6 +67,9 @@ public abstract class FileSystemUtils : IFileSystemUtils /// 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 8ddc2b83..f60b49c8 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.Parsers.Spdx22SbomParser/Entities/CreationInfo.cs b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/CreationInfo.cs index 7de88ab3..328fbb5c 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; } @@ -28,5 +29,6 @@ public class CreationInfo public IEnumerable Creators { get; set; } [JsonPropertyName("licenseListVersion")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 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 5e3e7cd9..7bcda609 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/FormatEnforcedSPDX2.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Entities/FormatEnforcedSPDX2.cs @@ -10,6 +10,7 @@ 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; } 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 00000000..e4a43659 --- /dev/null +++ b/test/Microsoft.Sbom.Api.Tests/Workflows/Helpers/SbomRedactorTests.cs @@ -0,0 +1,142 @@ +// 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; + +namespace Microsoft.Sbom.Api.Tests.Workflows.Helpers; + +[TestClass] +public class SbomRedactorTests +{ + private Mock mockValidatedSbom; + + private SbomRedactor testSubject; + + [TestInitialize] + public void Init() + { + mockValidatedSbom = new Mock(); + testSubject = new SbomRedactor(); + } + + [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 + { + 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 + { + 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 51a22a01..36423204 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,135 @@ 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(); + + 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(); + var result = await testSubject.RunAsync(); Assert.IsTrue(result); + var redactedResult = Encoding.ASCII.GetString(outStream.ToArray()); + Assert.AreEqual(redactedResult, /*lang=json,strict*/ @"{""comment"":null,""documentDescribes"":null,""files"":null,""packages"":null,""relationships"":null,""spdxVersion"":null,""dataLicense"":null,""SPDXID"":null,""name"":""redacted"",""documentNamespace"":null,""creationInfo"":null}"); + } + + 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(); } }