Skip to content

Commit

Permalink
Add redaction workflow logic
Browse files Browse the repository at this point in the history
  • Loading branch information
sfoslund committed May 21, 2024
1 parent b005f3f commit 8e295cf
Show file tree
Hide file tree
Showing 11 changed files with 516 additions and 10 deletions.
14 changes: 14 additions & 0 deletions src/Microsoft.Sbom.Api/FormatValidator/IValidatedSBOM.cs
Original file line number Diff line number Diff line change
@@ -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<FormatValidationResults> GetValidationResults();

public Task<FormatEnforcedSPDX2> GetRawSPDXDocument();
}
2 changes: 1 addition & 1 deletion src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOM.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/Microsoft.Sbom.Api/FormatValidator/ValidatedSBOMFactory.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
88 changes: 88 additions & 0 deletions src/Microsoft.Sbom.Api/Workflows/Helpers/SbomRedactor.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// SBOM redactor that removes file information from SBOMs
/// </summary>
public class SbomRedactor
{
private const string SpdxFileRelationshipPrefix = "SPDXRef-File-";

public virtual async Task<FormatEnforcedSPDX2> 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<SPDXRelationship>();
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);
}
}
129 changes: 126 additions & 3 deletions src/Microsoft.Sbom.Api/Workflows/SBOMRedactionWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -17,17 +24,133 @@ public class SbomRedactionWorkflow : IWorkflow<SbomRedactionWorkflow>

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<bool> 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<IDictionary<string, IValidatedSBOM>> GetValidSbomsAsync()
{
var sbomPaths = GetInputSbomPaths();
var validatedSboms = new Dictionary<string, IValidatedSBOM>();
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<string> GetInputSbomPaths()
{
if (configuration.SbomPath?.Value != null)
{
return new List<string>() { 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;
}
}
3 changes: 3 additions & 0 deletions src/Microsoft.Sbom.Common/FileSystemUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public abstract class FileSystemUtils : IFileSystemUtils
/// <inheritdoc />
public bool FileExists(string path) => File.Exists(path);

/// <inheritdoc />
public string GetFileName(string filePath) => Path.GetFileName(filePath);

/// <inheritdoc />
public Stream OpenWrite(string filePath) => new FileStream(
filePath,
Expand Down
7 changes: 7 additions & 0 deletions src/Microsoft.Sbom.Common/IFileSystemUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ public interface IFileSystemUtils
/// <returns>True if the file exists, false otherwise.</returns>
bool FileExists(string path);

/// <summary>
/// Get the file name of a file.
/// </summary>
/// <param name="filePath">The absolute path of the file.</param>
/// <returns>The file name.</returns>
string GetFileName(string filePath);

/// <summary>
/// Get the directory name of a file.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities;
/// </summary>
public class CreationInfo
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("comment")]
public string Comment { get; set; }

Expand All @@ -28,5 +29,6 @@ public class CreationInfo
public IEnumerable<string> Creators { get; set; }

[JsonPropertyName("licenseListVersion")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LicenseListVersion { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
Loading

0 comments on commit 8e295cf

Please sign in to comment.