Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/Microsoft.Sbom.Api/Workflows/Helpers/ConsolidationSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 Microsoft.Sbom.Common.Config;
using Microsoft.Sbom.Extensions;

namespace Microsoft.Sbom.Api.Workflows.Helpers;

/// <summary>
/// A class that lets us track information about the source artifact that feeds into a consolidated SBOM.
/// </summary>
internal class ConsolidationSource
{
public ArtifactInfo ArtifactInfo { get; }

public ISbomConfig SbomConfig { get; }

public string SbomPath { get; }

public ConsolidationSource(ArtifactInfo artifactInfo, ISbomConfig sbomConfig, string sbomPath)
{
ArtifactInfo = artifactInfo ?? throw new ArgumentNullException(nameof(artifactInfo));
SbomConfig = sbomConfig ?? throw new ArgumentNullException(nameof(sbomConfig));
SbomPath = sbomPath ?? throw new ArgumentNullException(nameof(sbomPath));
}

public override string ToString()
{
return $"ConsolidationSource: {ArtifactInfo}, ManifestInfo: {SbomConfig.ManifestInfo}, SbomPath: {SbomPath}";
}
}
48 changes: 21 additions & 27 deletions src/Microsoft.Sbom.Api/Workflows/SbomConsolidationWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Sbom.Api.Utils;
using Microsoft.Sbom.Api.Workflows.Helpers;
using Microsoft.Sbom.Common;
using Microsoft.Sbom.Common.Config;
using Microsoft.Sbom.Extensions;
Expand All @@ -21,30 +22,28 @@ public class SbomConsolidationWorkflow : IWorkflow<SbomConsolidationWorkflow>
private readonly ILogger logger;
private readonly IConfiguration configuration;
private readonly ISbomConfigFactory sbomConfigFactory;
private readonly ISPDXFormatDetector sPDXFormatDetector;
private readonly ISPDXFormatDetector spdxFormatDetector;
private readonly IFileSystemUtils fileSystemUtils;
private readonly IMetadataBuilderFactory metadataBuilderFactory;
private readonly IWorkflow<SbomGenerationWorkflow> sbomGenerationWorkflow;
private readonly IReadOnlyDictionary<ManifestInfo, IMergeableContentProvider> contentProviders;

private IReadOnlyDictionary<string, ArtifactInfo> ArtifactInfoMap => configuration.ArtifactInfoMap.Value;

internal IEnumerable<(ManifestInfo, string)> SourceSbomsTemp { get; set; } = Enumerable.Empty<(ManifestInfo, string)>(); // Stub property for testing, will remove soon

public SbomConsolidationWorkflow(
ILogger logger,
IConfiguration configuration,
IWorkflow<SbomGenerationWorkflow> sbomGenerationWorkflow,
ISbomConfigFactory sbomConfigFactory,
ISPDXFormatDetector sPDXFormatDetector,
ISPDXFormatDetector spdxFormatDetector,
IFileSystemUtils fileSystemUtils,
IMetadataBuilderFactory metadataBuilderFactory,
IEnumerable<IMergeableContentProvider> mergeableContentProviders)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
this.sbomConfigFactory = sbomConfigFactory ?? throw new ArgumentNullException(nameof(sbomConfigFactory));
this.sPDXFormatDetector = sPDXFormatDetector ?? throw new ArgumentNullException(nameof(sPDXFormatDetector));
this.spdxFormatDetector = spdxFormatDetector ?? throw new ArgumentNullException(nameof(spdxFormatDetector));
this.fileSystemUtils = fileSystemUtils ?? throw new ArgumentNullException(nameof(fileSystemUtils));
this.metadataBuilderFactory = metadataBuilderFactory ?? throw new ArgumentNullException(nameof(metadataBuilderFactory));
this.sbomGenerationWorkflow = sbomGenerationWorkflow ?? throw new ArgumentNullException(nameof(sbomGenerationWorkflow));
Expand All @@ -67,46 +66,46 @@ public SbomConsolidationWorkflow(
/// <inheritdoc/>
public virtual async Task<bool> RunAsync()
{
var sbomsToConsolidate = ArtifactInfoMap.Select(artifact => GetSbomsToConsolidate(artifact.Key, artifact.Value))
var consolidationSources = ArtifactInfoMap.Select(artifact => GetSbomsToConsolidate(artifact.Key, artifact.Value))
.Where(l => l != null)
.SelectMany(l => l);
if (sbomsToConsolidate == null || !sbomsToConsolidate.Any())
if (consolidationSources == null || !consolidationSources.Any())
{
logger.Information($"No valid SBOMs detected.");
return false;
}
else
{
logger.Information($"Running consolidation on the following SBOMs:\n{string.Join('\n', sbomsToConsolidate.Select(s => s.config.ManifestJsonFilePath))}");
logger.Information($"Running consolidation on the following SBOMs:\n{string.Join('\n', consolidationSources.Select(s => s.SbomConfig.ManifestJsonFilePath))}");
}

return await ValidateSourceSbomsAsync(sbomsToConsolidate) && await GenerateConsolidatedSbom();
return await ValidateSourceSbomsAsync(consolidationSources) && await GenerateConsolidatedSbom(consolidationSources);
}

private IEnumerable<(ISbomConfig config, ArtifactInfo info)> GetSbomsToConsolidate(string artifactPath, ArtifactInfo info)
private IEnumerable<ConsolidationSource> GetSbomsToConsolidate(string artifactPath, ArtifactInfo info)
{
var manifestDirPath = info?.ExternalManifestDir ?? fileSystemUtils.JoinPaths(artifactPath, Constants.ManifestFolder);
var isValidSpdxFormat = sPDXFormatDetector.TryGetSbomsWithVersion(manifestDirPath, out var detectedSboms);
var isValidSpdxFormat = spdxFormatDetector.TryGetSbomsWithVersion(manifestDirPath, out var detectedSboms);
if (!isValidSpdxFormat)
{
logger.Information($"No SBOMs located in {manifestDirPath} of a recognized SPDX format.");
return null;
}

return detectedSboms.Select((sbom) => (sbomConfigFactory.Get(sbom.manifestInfo, manifestDirPath, metadataBuilderFactory), info));
return detectedSboms.Select((sbom) => new ConsolidationSource(info, sbomConfigFactory.Get(sbom.manifestInfo, manifestDirPath, metadataBuilderFactory), sbom.sbomFilePath));
}

private async Task<bool> ValidateSourceSbomsAsync(IEnumerable<(ISbomConfig config, ArtifactInfo info)> sbomsToValidate)
private async Task<bool> ValidateSourceSbomsAsync(IEnumerable<ConsolidationSource> consolidationSources)
{
var validationWorkflows = sbomsToValidate
var validationWorkflows = consolidationSources
.Select(async sbom => await Task.FromResult(true)); // TODO: Run validation workflow
var results = await Task.WhenAll(validationWorkflows);
return results.All(b => b);
}

private async Task<bool> GenerateConsolidatedSbom()
private async Task<bool> GenerateConsolidatedSbom(IEnumerable<ConsolidationSource> consolidationSources)
{
if (!TryGetMergeableContent(out var mergeableContents))
if (!TryGetMergeableContent(consolidationSources, out var mergeableContents))
{
return false;
}
Expand All @@ -115,25 +114,20 @@ private async Task<bool> GenerateConsolidatedSbom()
return await sbomGenerationWorkflow.RunAsync().ConfigureAwait(false);
}

private bool TryGetMergeableContent(out IEnumerable<MergeableContent> mergeableContents)
private bool TryGetMergeableContent(IEnumerable<ConsolidationSource> consolidationSources, out IEnumerable<MergeableContent> mergeableContents)
{
mergeableContents = null; // Until proven otherwise

var contents = new List<MergeableContent>();

if (!SourceSbomsTemp.Any())
{
logger.Error("No source SBOMs provided for consolidation.");
return false;
}

// Incorporate the source SBOMs in the consolidated SBOM generation workflow.
foreach (var sourceSbom in SourceSbomsTemp)
foreach (var consolidationSource in consolidationSources)
{
var (manifestInfo, sbomPath) = sourceSbom;
if (!contentProviders.TryGetValue(manifestInfo, out var contentProvider))
var sbomConfig = consolidationSource.SbomConfig;
var sbomPath = consolidationSource.SbomPath;
if (!contentProviders.TryGetValue(sbomConfig.ManifestInfo, out var contentProvider))
{
logger.Error("No content provider found for manifest info: {ManifestInfo}", manifestInfo);
logger.Error("No content provider found for manifest info: {ManifestInfo}", sbomConfig.ManifestInfo);
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,25 @@ namespace Microsoft.Sbom.Api.Workflows.Tests;
[TestClass]
public class SbomConsolidationWorkflowTests
{
private const string SPDX22FilePath = "rootpath/_manifest/spdx_2.2/manifest.spdx.json";
private const string ArtifactKey1 = "sbom-key-1";
private const string ArtifactKey2 = "sbom-key-2";
private const string ExternalManifestDir2 = "external-manifest-dir-2";

private Mock<ILogger> loggerMock;
private Mock<IConfiguration> configurationMock;
private Mock<IWorkflow<SbomGenerationWorkflow>> sbomGenerationWorkflowMock;
private Mock<IMergeableContentProvider> mergeableContent22ProviderMock;
private Mock<IMergeableContentProvider> mergeableContent30ProviderMock;
private Mock<ISbomConfigFactory> sbomConfigFactoryMock;
private Mock<ISPDXFormatDetector> sPDXFormatDetectorMock;
private Mock<ISPDXFormatDetector> spdxFormatDetectorMock;
private Mock<IFileSystemUtils> fileSystemUtilsMock;
private Mock<IMetadataBuilderFactory> metadataBuilderFactoryMock;
private SbomConsolidationWorkflow testSubject;

private Dictionary<string, ArtifactInfo> artifactInfoMapStub = new Dictionary<string, ArtifactInfo>()
{
{ "sbom-key-1", new ArtifactInfo() { } },
{ "sbom-key-2", new ArtifactInfo() { ExternalManifestDir = "external-manifest-dir-2" } },
{ ArtifactKey1, new ArtifactInfo() { } },
{ ArtifactKey2, new ArtifactInfo() { ExternalManifestDir = ExternalManifestDir2 } },
};

[TestInitialize]
Expand All @@ -45,23 +48,26 @@ public void BeforeEachTest()
configurationMock = new Mock<IConfiguration>(MockBehavior.Strict);
sbomGenerationWorkflowMock = new Mock<IWorkflow<SbomGenerationWorkflow>>(MockBehavior.Strict);
sbomConfigFactoryMock = new Mock<ISbomConfigFactory>(MockBehavior.Strict);
sPDXFormatDetectorMock = new Mock<ISPDXFormatDetector>(MockBehavior.Strict);
spdxFormatDetectorMock = new Mock<ISPDXFormatDetector>(MockBehavior.Strict);
fileSystemUtilsMock = new Mock<IFileSystemUtils>(MockBehavior.Strict);
metadataBuilderFactoryMock = new Mock<IMetadataBuilderFactory>(MockBehavior.Strict);
mergeableContent22ProviderMock = new Mock<IMergeableContentProvider>(MockBehavior.Strict);
mergeableContent30ProviderMock = new Mock<IMergeableContentProvider>(MockBehavior.Strict);

mergeableContent22ProviderMock.Setup(m => m.ManifestInfo)
.Returns(Constants.SPDX22ManifestInfo);
mergeableContent30ProviderMock.Setup(m => m.ManifestInfo)
.Returns(Constants.SPDX30ManifestInfo);

testSubject = new SbomConsolidationWorkflow(
loggerMock.Object,
configurationMock.Object,
sbomGenerationWorkflowMock.Object,
sbomConfigFactoryMock.Object,
sPDXFormatDetectorMock.Object,
spdxFormatDetectorMock.Object,
fileSystemUtilsMock.Object,
metadataBuilderFactoryMock.Object,
new[] { mergeableContent22ProviderMock.Object });
new[] { mergeableContent22ProviderMock.Object, mergeableContent30ProviderMock.Object });
}

[TestCleanup]
Expand All @@ -71,7 +77,7 @@ public void AfterEachTest()
configurationMock.VerifyAll();
sbomGenerationWorkflowMock.VerifyAll();
sbomConfigFactoryMock.VerifyAll();
sPDXFormatDetectorMock.VerifyAll();
spdxFormatDetectorMock.VerifyAll();
fileSystemUtilsMock.VerifyAll();
metadataBuilderFactoryMock.VerifyAll();
mergeableContent22ProviderMock.VerifyAll();
Expand Down Expand Up @@ -105,7 +111,7 @@ public async Task RunAsync_ReturnsFalseOnNoValidSpdxSbomsFound()
}

IList<(string, ManifestInfo)> detectedSboms;
sPDXFormatDetectorMock
spdxFormatDetectorMock
.Setup(m => m.TryGetSbomsWithVersion(artifactInfo.ExternalManifestDir ?? key, out detectedSboms))
.Returns(false);
}
Expand All @@ -122,13 +128,14 @@ public async Task RunAsync_MinimalHappyPath_CallsGenerationWorkflow(bool expecte
SetUpSbomsToValidate();
sbomGenerationWorkflowMock.Setup(x => x.RunAsync())
.ReturnsAsync(expectedResult);
mergeableContent22ProviderMock.Setup(x => x.TryGetContent(SPDX22FilePath, out It.Ref<MergeableContent>.IsAny))
mergeableContent22ProviderMock.Setup(x => x.TryGetContent(ArtifactKey1, out It.Ref<MergeableContent>.IsAny))
.Returns(true);
mergeableContent22ProviderMock.Setup(x => x.TryGetContent(ExternalManifestDir2, out It.Ref<MergeableContent>.IsAny))
.Returns(true);
mergeableContent30ProviderMock.Setup(x => x.TryGetContent(ArtifactKey1, out It.Ref<MergeableContent>.IsAny))
.Returns(true);
mergeableContent30ProviderMock.Setup(x => x.TryGetContent(ExternalManifestDir2, out It.Ref<MergeableContent>.IsAny))
.Returns(true);

testSubject.SourceSbomsTemp = new List<(ManifestInfo, string)>
{
(Constants.SPDX22ManifestInfo, SPDX22FilePath),
};

var result = await testSubject.RunAsync();
Assert.AreEqual(expectedResult, result);
Expand All @@ -155,7 +162,7 @@ private void SetUpSbomsToValidate()
(manifestDirPath, Constants.SPDX22ManifestInfo),
(manifestDirPath, Constants.SPDX30ManifestInfo)
};
sPDXFormatDetectorMock
spdxFormatDetectorMock
.Setup(m => m.TryGetSbomsWithVersion(manifestDirPath, out res))
.Returns(true);
sbomConfigFactoryMock
Expand Down