diff --git a/.gitignore b/.gitignore
index 94f3f90dc..7f5dec04f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -189,6 +189,7 @@ PublishScripts/
# NuGet Packages
*.nupkg
+*.nuspec
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1eea610a5..8c75642e4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,6 +14,11 @@
+
+
+
+
+
@@ -22,7 +27,7 @@
-
+
@@ -44,6 +49,7 @@
+
diff --git a/Microsoft.Sbom.sln b/Microsoft.Sbom.sln
index 199c973ba..643283e93 100644
--- a/Microsoft.Sbom.sln
+++ b/Microsoft.Sbom.sln
@@ -49,8 +49,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Sbom.Extensions.D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Sbom.Extensions.DependencyInjection.Tests", "test\Microsoft.Sbom.Extensions.DependencyInjection.Tests\Microsoft.Sbom.Extensions.DependencyInjection.Tests.csproj", "{EE4E2E03-7B4C-46E5-B9D2-89E84A18D787}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Sbom.Targets", "src\Microsoft.Sbom.Targets\Microsoft.Sbom.Targets.csproj", "{E6C3C851-EEA0-466E-BA36-73ED85F13EEA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Sbom.Targets.Tests", "test\Microsoft.Sbom.Targets.Tests\Microsoft.Sbom.Targets.Tests.csproj", "{E31B914C-F24B-4DC8-ACC7-CAEA952563B8}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Sbom.Tool.Tests", "test\Microsoft.Sbom.Tool.Tests\Microsoft.Sbom.Tool.Tests.csproj", "{FC5A9799-7C44-4BFA-BA22-55DCAF1A1B9F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Sbom.Targets.E2E.Tests", "test\Microsoft.Sbom.Targets.E2E.Tests\Microsoft.Sbom.Targets.E2E.Tests.csproj", "{3FDE7800-F61F-4C45-93AB-648A4C7979C7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -109,10 +115,22 @@ Global
{EE4E2E03-7B4C-46E5-B9D2-89E84A18D787}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE4E2E03-7B4C-46E5-B9D2-89E84A18D787}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE4E2E03-7B4C-46E5-B9D2-89E84A18D787}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6C3C851-EEA0-466E-BA36-73ED85F13EEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6C3C851-EEA0-466E-BA36-73ED85F13EEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6C3C851-EEA0-466E-BA36-73ED85F13EEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6C3C851-EEA0-466E-BA36-73ED85F13EEA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E31B914C-F24B-4DC8-ACC7-CAEA952563B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E31B914C-F24B-4DC8-ACC7-CAEA952563B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E31B914C-F24B-4DC8-ACC7-CAEA952563B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E31B914C-F24B-4DC8-ACC7-CAEA952563B8}.Release|Any CPU.Build.0 = Release|Any CPU
{FC5A9799-7C44-4BFA-BA22-55DCAF1A1B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC5A9799-7C44-4BFA-BA22-55DCAF1A1B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC5A9799-7C44-4BFA-BA22-55DCAF1A1B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC5A9799-7C44-4BFA-BA22-55DCAF1A1B9F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3FDE7800-F61F-4C45-93AB-648A4C7979C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3FDE7800-F61F-4C45-93AB-648A4C7979C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3FDE7800-F61F-4C45-93AB-648A4C7979C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3FDE7800-F61F-4C45-93AB-648A4C7979C7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/nuget.config b/nuget.config
index 248a5bb51..765346e53 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,7 +1,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/pipelines/sbom-tool-main-build.yaml b/pipelines/sbom-tool-main-build.yaml
index 22593f0ed..ed5733ba0 100644
--- a/pipelines/sbom-tool-main-build.yaml
+++ b/pipelines/sbom-tool-main-build.yaml
@@ -105,7 +105,7 @@ extends:
]
condition: and(succeeded(), startswith(variables['Build.SourceBranch'], 'refs/tags/'))
- - powershell: 'dotnet pack Microsoft.Sbom.sln -c $(BuildConfiguration) --no-restore --no-build -o $(Build.ArtifactStagingDirectory)/nuget --include-symbols -p:SymbolPackageFormat=snupkg'
+ - powershell: 'Get-ChildItem -Recurse -Filter *.csproj -Path src | ForEach-Object { dotnet pack $_.FullName -c $(BuildConfiguration) --no-restore --no-build -o $(Build.ArtifactStagingDirectory)/nuget --include-symbols -p:SymbolPackageFormat=snupkg }'
displayName: 'Pack NuGet package'
- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@3
diff --git a/src/Microsoft.Sbom.Targets/GenerateSbom.cs b/src/Microsoft.Sbom.Targets/GenerateSbom.cs
new file mode 100644
index 000000000..0bdd6ff5f
--- /dev/null
+++ b/src/Microsoft.Sbom.Targets/GenerateSbom.cs
@@ -0,0 +1,102 @@
+// 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.Targets;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using Microsoft.Build.Framework;
+
+///
+/// This partial class defines and sanitizes the arguments that will be passed
+/// into the SBOM API and CLI tool for generation.
+///
+public partial class GenerateSbom
+{
+ ///
+ /// Gets or sets the path to the drop directory for which the SBOM will be generated.
+ ///
+ [Required]
+ public string BuildDropPath { get; set; }
+
+ ///
+ /// Gets or sets the supplier of the package the SBOM represents.
+ ///
+ [Required]
+ public string PackageSupplier { get; set; }
+
+ ///
+ /// Gets or sets the name of the package the SBOM represents.
+ ///
+ [Required]
+ public string PackageName { get; set; }
+
+ ///
+ /// Gets or sets the version of the package the SBOM represents.
+ ///
+ [Required]
+ public string PackageVersion { get; set; }
+
+ ///
+ /// Gets or sets the base path of the SBOM namespace uri.
+ ///
+ [Required]
+ public string NamespaceBaseUri { get; set; }
+
+ ///
+ /// Gets or sets the path to the directory containing build components and package information.
+ /// For example, path to a .csproj or packages.config file.
+ ///
+ public string BuildComponentPath { get; set; }
+
+ ///
+ /// Gets or sets a unique URI part that will be appended to NamespaceBaseUri.
+ ///
+ public string NamespaceUriUniquePart { get; set; }
+
+ ///
+ /// Gets or sets the path to a file containing a list of external SBOMs that will be appended to the
+ /// SBOM that is being generated.
+ ///
+ public string ExternalDocumentListFile { get; set; }
+
+ ///
+ /// Indicates whether licensing information will be fetched for detected packages.
+ ///
+ public bool FetchLicenseInformation { get; set; }
+
+ ///
+ /// Indicates whether to parse licensing and supplier information from a packages metadata file.
+ ///
+ public bool EnablePackageMetadataParsing { get; set; }
+
+ ///
+ /// Gets or sets the verbosity level for logging output.
+ ///
+ public string Verbosity { get; set; }
+
+ ///
+ /// Gets or sets a list of names and versions of the manifest format being used.
+ ///
+ public string ManifestInfo { get; set; }
+
+ ///
+ /// Indicates whether the previously generated SBOM manifest directory should be deleted
+ /// before generating a new SBOM in the directory specified by ManifestDirPath.
+ /// Defaults to true.
+ ///
+ public bool DeleteManifestDirIfPresent { get; set; } = true;
+
+ ///
+ /// Gets or sets the path where the SBOM will be generated. For now, this property
+ /// will be unset as the _manifest directory is intended to be at the root of a NuGet package
+ /// specified by BuildDropPath.
+ ///
+ public string ManifestDirPath { get; set; }
+
+ ///
+ /// Gets or sets the path to the SBOM CLI tool
+ ///
+ public string SbomToolPath { get; set; }
+}
diff --git a/src/Microsoft.Sbom.Targets/GenerateSbomTask.cs b/src/Microsoft.Sbom.Targets/GenerateSbomTask.cs
new file mode 100644
index 000000000..b817ea3bd
--- /dev/null
+++ b/src/Microsoft.Sbom.Targets/GenerateSbomTask.cs
@@ -0,0 +1,125 @@
+// 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.Targets;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using System.IO;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Sbom.Api.Manifest.ManifestConfigHandlers;
+using Microsoft.Sbom.Api.Metadata;
+using Microsoft.Sbom.Api.Providers;
+using Microsoft.Sbom.Api.Providers.ExternalDocumentReferenceProviders;
+using Microsoft.Sbom.Api.Providers.FilesProviders;
+using Microsoft.Sbom.Api.Providers.PackagesProviders;
+using Microsoft.Sbom.Contracts;
+using Microsoft.Sbom.Contracts.Entities;
+using Microsoft.Sbom.Contracts.Interfaces;
+using Microsoft.Sbom.Extensions;
+using Microsoft.Sbom.Extensions.DependencyInjection;
+using Microsoft.Sbom.Parsers.Spdx22SbomParser;
+
+///
+/// MSBuild task for generating SBOMs from build output.
+///
+public partial class GenerateSbom : Task
+{
+ private ISBOMGenerator Generator { get; set; }
+
+ ///
+ /// Constructor for the GenerateSbomTask.
+ ///
+ public GenerateSbom()
+ {
+ var host = Host.CreateDefaultBuilder()
+ .ConfigureServices((host, services) =>
+ services
+ .AddSbomTool()
+ /* Manually adding some dependencies since `AddSbomTool()` does not add them when
+ * running the MSBuild Task from another project.
+ */
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton())
+ .Build();
+ this.Generator = host.Services.GetRequiredService();
+ }
+
+ ///
+ public override bool Execute()
+ {
+ try
+ {
+ // Validate required args and args that take paths as input.
+ if (!ValidateAndSanitizeRequiredParams() || !ValidateAndSanitizeNamespaceUriUniquePart())
+ {
+ return false;
+ }
+
+ // Set other configurations. The GenerateSBOMAsync() already sanitizes and checks for
+ // a valid namespace URI and generates a random guid for NamespaceUriUniquePart if
+ // one is not provided.
+ var sbomMetadata = new SBOMMetadata
+ {
+ PackageSupplier = this.PackageSupplier,
+ PackageName = this.PackageName,
+ PackageVersion = this.PackageVersion,
+ };
+ var runtimeConfiguration = new RuntimeConfiguration
+ {
+ NamespaceUriBase = this.NamespaceBaseUri,
+ NamespaceUriUniquePart = this.NamespaceUriUniquePart,
+ DeleteManifestDirectoryIfPresent = this.DeleteManifestDirIfPresent,
+ Verbosity = ValidateAndAssignVerbosity(),
+ };
+#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
+ var result = System.Threading.Tasks.Task.Run(() => this.Generator.GenerateSbomAsync(
+ rootPath: this.BuildDropPath,
+ manifestDirPath: this.ManifestDirPath,
+ metadata: sbomMetadata,
+ componentPath: this.BuildComponentPath,
+ runtimeConfiguration: runtimeConfiguration,
+ specifications: ValidateAndAssignSpecifications(),
+ externalDocumentReferenceListFile: this.ExternalDocumentListFile)).GetAwaiter().GetResult();
+#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
+
+ return result.IsSuccessful;
+ }
+ catch (Exception e)
+ {
+ Log.LogError($"SBOM generation failed: {e.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Check for ManifestInfo and create an SbomSpecification accordingly.
+ ///
+ /// A list of the parsed manifest info. Null if the manifest info is null or empty.
+ private IList ValidateAndAssignSpecifications()
+ {
+ if (!string.IsNullOrWhiteSpace(this.ManifestInfo))
+ {
+ return [SbomSpecification.Parse(this.ManifestInfo)];
+ }
+
+ return null;
+ }
+}
diff --git a/src/Microsoft.Sbom.Targets/Microsoft.Sbom.Targets.csproj b/src/Microsoft.Sbom.Targets/Microsoft.Sbom.Targets.csproj
new file mode 100644
index 000000000..bcdcdf15c
--- /dev/null
+++ b/src/Microsoft.Sbom.Targets/Microsoft.Sbom.Targets.csproj
@@ -0,0 +1,84 @@
+
+
+
+ Microsoft.Sbom.Targets
+ net6.0;net8.0;net472
+ win-x64;osx-x64;linux-x64
+ true
+ true
+ true
+ 1.0.0
+ GenerateSbomTask
+ Tasks and targets for running the SBOM tool.
+ true
+ net8.0
+
+
+
+ $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage
+
+
+ tasks
+ true
+
+ NU5100
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+ true
+ \tasks\net472\sbom-tool\
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Sbom.Targets/Microsoft.Sbom.Targets.targets b/src/Microsoft.Sbom.Targets/Microsoft.Sbom.Targets.targets
new file mode 100644
index 000000000..12e9bd691
--- /dev/null
+++ b/src/Microsoft.Sbom.Targets/Microsoft.Sbom.Targets.targets
@@ -0,0 +1,79 @@
+
+
+ net472
+ net8.0
+
+ $([System.IO.Path]::Combine($(MSBuildThisFileDirectory),..,tasks,$(GenerateSbom_TFM),sbom-tool))
+ $([System.IO.Path]::Combine($(MSBuildThisFileDirectory),..,tasks,$(GenerateSbom_TFM),Microsoft.Sbom.Targets.dll))
+
+
+ $(SbomToolBinaryOutputPath)
+ _manifest
+ spdx_2.2
+
+
+
+
+
+
+ false
+ $(MSBuildProjectDirectory)
+ $(Authors)
+ $(AssemblyName)
+ $(PackageId)
+ $(AssemblyName)
+ $(Version)
+ 1.0.0
+ http://spdx.org/spdxdocs/$(SbomGenerationPackageName)
+ false
+ false
+ information
+ SPDX:2.2
+ true
+ $([System.Guid]::NewGuid())
+ $([System.String]::Copy('$(UnzipGuid)').Substring(0, 8))
+
+
+
+
+
+
+ $([System.IO.Path]::GetFullPath('$(PackageOutputPath)'))
+
+
+ $([System.IO.Path]::Combine($(PackageOutputFullPath), $(PackageId).$(PackageVersion).nupkg))
+
+
+ $([System.IO.Path]::Combine($(PackageOutputFullPath), $(PackageId).$(PackageVersion).$(ShortUnzipGuidFolder).temp))
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Sbom.Targets/README.md b/src/Microsoft.Sbom.Targets/README.md
new file mode 100644
index 000000000..df74d50b9
--- /dev/null
+++ b/src/Microsoft.Sbom.Targets/README.md
@@ -0,0 +1,53 @@
+# SBOM Generation for .NET Projects
+## Microsoft.Sbom.Targets
+This project implements a custom MSBuild task that generates an SBOM using the SBOM API and CLI tool. The MSBuild task binaries along with the associated targets are packaged as a NuGet package and can be consumed within a .NET project. Once installed, an SBOM will automatically be generated upon packing the .NET project.
+
+## MSBuild Task Implementation
+The custom MSBuild task is implemented across the following partial classes:
+- `GenerateSbom.cs`
+- `GenerateSbomTask.cs`
+- `SbomCLIToolTask.cs`
+- `SbomInputValidator.cs`
+
+Due to differences in [MSBuild versions](https://learn.microsoft.com/en-us/visualstudio/msbuild/tutorial-custom-task-code-generation?view=vs-2022#create-the-appsettingstronglytyped-project) between Visual Studio and the .Net Core CLI tool, the SBOM generation logic needed to be split into two parts:
+
+1) `GenerateSbomTask.cs` is invoked if the MSBuild version targets the "Core" (.NET Core) runtime bundled with the .NET Core CLI tool. This class utilizes the SBOM API to generate an SBOM.
+
+2) `SbomCLIToolTask.cs` is invoked if the MSBuild version targets the "Full" (.NET Framework) runtime bundled with Visual Studio. Because the SBOM API does not support .NET Framework, this class utilizes the SBOM CLI Tool to generate an SBOM.
+
+Finally, the `Microsoft.Sbom.Targets.targets` file creates a target that will execute the custom MSBuild task. This file will be automatically imported when consuming the NuGet package.
+
+## SBOM Generation Properties
+The custom MSBuild task accepts most of the arguments available for the [SBOM CLI Tool](../../docs/sbom-tool-arguments.md). After the .targets file is imported into a .NET project, the following properties can be set:
+
+| Property | Default Value | Required |
+|-----------------------------------------------------|-------------|---------|
+| `` | `false` | No. To enable SBOM generation, set this to true. |
+| `` | `$(MSBuildProjectDirectory)` | No |
+| `` | `$(Authors)`. If `$(Authors)` is null, it will set `$(AssemblyName)` | Yes |
+| `` | `$(PackageId)`. If `$(PackageId)` is null, it will set `$(AssemblyName)` | Yes |
+| `` | `$(Version)`. If `$(Version)` is null, it will set "1.0.0" | Yes |
+| `` | `http://spdx.org/spdxdocs/$(SbomGenerationPackageName)` | Yes |
+| `` | N/A | No |
+| `` | N/A | No |
+| `` | `false` | No |
+| `` | `false` | No |
+| `` | `Information` | No |
+| `` | `SPDX:2.2` | No |
+| `` | `true` | No |
+
+## Local SBOM Generation Workflow
+After building the Microsoft.Sbom.Targets project, it will generate a NuGet package containing the MSBuild task's binaries and associated .targets file in the `bin\$(Configuration)` folder. The following steps describe how to consume this NuGet package and generate an SBOM:
+
+1) Create a sample .NET project.
+2) Open the project's NuGet package manager.
+3) Add the path to the Microsoft.Sbom.Targets NuGet package as a package source. You can name it "Local".
+4) Look for the Microsoft.Sbom.Targets package within the package manager and install it.
+5) Add the following to your sample project's .csproj file:
+```
+
+ true
+
+```
+6) Build the sample project.
+7) Pack the sample project. The SBOM will be generated under the `_manifest` folder at the root of the NuGet package.
diff --git a/src/Microsoft.Sbom.Targets/SbomCLIToolTask.cs b/src/Microsoft.Sbom.Targets/SbomCLIToolTask.cs
new file mode 100644
index 000000000..396563dd1
--- /dev/null
+++ b/src/Microsoft.Sbom.Targets/SbomCLIToolTask.cs
@@ -0,0 +1,103 @@
+// 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.Targets;
+
+using System;
+using System.IO;
+using Microsoft.Build.Utilities;
+
+///
+/// MSBuild ToolTask for generating an SBOM using the SBOM CLI tool
+///
+public partial class GenerateSbom : ToolTask
+{
+ protected override string ToolName => "Microsoft.Sbom.Tool";
+
+ ///
+ /// Get full path to SBOM CLI tool.
+ ///
+ ///
+ protected override string GenerateFullPathToTool()
+ {
+ return Path.Combine(this.SbomToolPath, $"{this.ToolName}.exe");
+ }
+
+ ///
+ /// Return a formatted list of arguments for the SBOM CLI tool.
+ ///
+ /// string list of args
+ protected override string GenerateCommandLineCommands()
+ {
+ var builder = new CommandLineBuilder();
+
+ builder.AppendSwitch("generate");
+ builder.AppendSwitchIfNotNull("-BuildDropPath ", this.BuildDropPath);
+ builder.AppendSwitchIfNotNull("-BuildComponentPath ", this.BuildComponentPath);
+ builder.AppendSwitchIfNotNull("-PackageName ", this.PackageName);
+ builder.AppendSwitchIfNotNull("-PackageVersion ", this.PackageVersion);
+ builder.AppendSwitchIfNotNull("-PackageSupplier ", this.PackageSupplier);
+ builder.AppendSwitchIfNotNull("-NamespaceUriBase ", this.NamespaceBaseUri);
+ builder.AppendSwitchIfNotNull("-DeleteManifestDirIfPresent ", $"{this.DeleteManifestDirIfPresent}");
+ builder.AppendSwitchIfNotNull("-FetchLicenseInformation ", $"{this.FetchLicenseInformation}");
+ builder.AppendSwitchIfNotNull("-EnablePackageMetadataParsing ", $"{this.EnablePackageMetadataParsing}");
+ builder.AppendSwitchIfNotNull("-Verbosity ", this.Verbosity);
+
+ // For optional arguments, append them only if they are specified by the user
+ if (!string.IsNullOrWhiteSpace(this.ManifestDirPath))
+ {
+ builder.AppendSwitchIfNotNull("-ManifestDirPath ", this.ManifestDirPath);
+ }
+
+ if (!string.IsNullOrWhiteSpace(this.ExternalDocumentListFile))
+ {
+ builder.AppendSwitchIfNotNull("-ExternalDocumentListFile ", this.ExternalDocumentListFile);
+ }
+
+ if (!string.IsNullOrWhiteSpace(this.NamespaceUriUniquePart))
+ {
+ builder.AppendSwitchIfNotNull("-NamespaceUriUniquePart ", this.NamespaceUriUniquePart);
+ }
+
+ if (!string.IsNullOrWhiteSpace(this.ManifestInfo))
+ {
+ builder.AppendSwitchIfNotNull("-ManifestInfo ", this.ManifestInfo);
+ }
+
+ return builder.ToString();
+ }
+
+ ///
+ /// Validates the SBOM CLI tool parameters
+ ///
+ ///
+ protected override bool ValidateParameters()
+ {
+ // Validate required args and args that take paths as input.
+ if (!ValidateAndSanitizeRequiredParams() || !ValidateAndSanitizeNamespaceUriUniquePart())
+ {
+ return false;
+ }
+
+ ValidateAndAssignVerbosity();
+ SetOutputImportance();
+ return true;
+ }
+
+ ///
+ /// This method sets the standard output importance. Setting
+ /// it to "High" ensures all output from the SBOM CLI is printed to
+ /// Visual Studio's output console; otherwise, it is hidden.
+ ///
+ private void SetOutputImportance()
+ {
+ this.StandardOutputImportance = "High";
+
+ if (this.Verbosity.ToLower().Equals("fatal"))
+ {
+ this.StandardOutputImportance = "Low";
+ }
+
+ this.LogStandardErrorAsError = true;
+ }
+}
diff --git a/src/Microsoft.Sbom.Targets/SbomInputValidator.cs b/src/Microsoft.Sbom.Targets/SbomInputValidator.cs
new file mode 100644
index 000000000..747595eed
--- /dev/null
+++ b/src/Microsoft.Sbom.Targets/SbomInputValidator.cs
@@ -0,0 +1,127 @@
+// 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.Targets;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+
+///
+/// Validation class used to sanitize and validate arguments passed into
+/// the GenerateSbomTask and SbomCLIToolTask
+///
+public partial class GenerateSbom
+{
+ private const string DefaultVerbosity = "Information";
+ private const EventLevel DefaultEventLevel = EventLevel.Informational;
+
+ ///
+ /// Ensure all required arguments are non-null/empty,
+ /// and do not contain whitespaces, tabs, or newline characters.
+ ///
+ /// True if the required parameters are valid. False otherwise.
+ public bool ValidateAndSanitizeRequiredParams()
+ {
+ var requiredProperties = new Dictionary
+ {
+ { nameof(this.BuildDropPath), this.BuildDropPath },
+ { nameof(this.PackageSupplier), this.PackageSupplier },
+ { nameof(this.PackageName), this.PackageName },
+ { nameof(this.PackageVersion), this.PackageVersion },
+ { nameof(this.NamespaceBaseUri), this.NamespaceBaseUri }
+ };
+
+ foreach (var property in requiredProperties)
+ {
+ if (string.IsNullOrWhiteSpace(property.Value))
+ {
+ Log.LogError($"SBOM generation failed: Empty argument detected for {property.Key}. Please provide a valid value.");
+ return false;
+ }
+ }
+
+ this.PackageSupplier = Remove_Spaces_Tabs_Newlines(this.PackageSupplier);
+ this.PackageName = Remove_Spaces_Tabs_Newlines(this.PackageName);
+ this.PackageVersion = Remove_Spaces_Tabs_Newlines(this.PackageVersion);
+ this.NamespaceBaseUri = this.NamespaceBaseUri.Trim();
+ this.BuildDropPath = this.BuildDropPath.Trim();
+
+ return true;
+ }
+
+ public string Remove_Spaces_Tabs_Newlines(string value)
+ {
+ return value.Replace("\n", string.Empty).Replace("\t", string.Empty).Replace(" ", string.Empty);
+ }
+
+ ///
+ /// Checks the user's input for Verbosity and assigns the
+ /// associated EventLevel value for logging. The SBOM API accepts
+ /// an EventLevel for verbosity while the CLI accepts LogEventLevel.
+ ///
+ public EventLevel ValidateAndAssignVerbosity()
+ {
+ // The following shows the accepted verbosity inputs for the SBOM CLI and API respectively
+ // *********************************
+ // The SBOM CLI | The SBOM API |
+ // *********************************
+ // Verbose | EventLevel.Verbose
+ // Debug | EventLevel.LogAlways
+ // Information | EventLevel.Informational
+ // Warning | EventLevel.Warning
+ // Error | EventLevel.Error
+ // Fatal | EventLevel.Critical
+
+ // We should standardize on the SBOM CLI verbosity inputs and convert them to the associated
+ // EventLevel value for the API.
+ if (string.IsNullOrWhiteSpace(this.Verbosity))
+ {
+ Log.LogWarning($"No verbosity level specified. Setting verbosity level at {DefaultVerbosity}.");
+ this.Verbosity = DefaultVerbosity;
+ return DefaultEventLevel;
+ }
+
+ switch (this.Verbosity.ToLower().Trim())
+ {
+ case "verbose":
+ return EventLevel.Verbose;
+ case "debug":
+ return EventLevel.Verbose;
+ case "information":
+ return EventLevel.Informational;
+ case "warning":
+ return EventLevel.Warning;
+ case "error":
+ return EventLevel.Error;
+ case "fatal":
+ return EventLevel.Critical;
+ default:
+ Log.LogWarning($"Unrecognized verbosity level specified. Setting verbosity level at {DefaultVerbosity}.");
+ this.Verbosity = DefaultVerbosity;
+ return DefaultEventLevel;
+ }
+ }
+
+ ///
+ /// Ensure a valid NamespaceUriUniquePart is provided.
+ ///
+ /// True if the Namespace URI unique part is valid. False otherwise.
+ public bool ValidateAndSanitizeNamespaceUriUniquePart()
+ {
+ // Ensure the NamespaceUriUniquePart is valid if provided.
+ if (!string.IsNullOrWhiteSpace(this.NamespaceUriUniquePart)
+ && (!Guid.TryParse(this.NamespaceUriUniquePart, out _)
+ || this.NamespaceUriUniquePart.Equals(Guid.Empty.ToString())))
+ {
+ Log.LogError($"SBOM generation failed: NamespaceUriUniquePart '{this.NamespaceUriUniquePart}' must be a valid unique GUID.");
+ return false;
+ }
+ else if (!string.IsNullOrWhiteSpace(this.NamespaceUriUniquePart))
+ {
+ this.NamespaceUriUniquePart = this.NamespaceUriUniquePart.Trim();
+ }
+
+ return true;
+ }
+}
diff --git a/test/Microsoft.Sbom.Targets.E2E.Tests/GenerateSbomE2ETests.cs b/test/Microsoft.Sbom.Targets.E2E.Tests/GenerateSbomE2ETests.cs
new file mode 100644
index 000000000..297577bda
--- /dev/null
+++ b/test/Microsoft.Sbom.Targets.E2E.Tests/GenerateSbomE2ETests.cs
@@ -0,0 +1,334 @@
+// 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.Targets.E2E.Tests;
+
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Microsoft.Build.Evaluation;
+using Microsoft.Build.Locator;
+using Microsoft.Build.Logging;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class GenerateSbomE2ETests
+{
+ /*
+ * The following tests validate the end-to-end workflow for importing the Microsoft.Sbom.Targets.targets
+ * into a .NET project, building it, packing it, and validating the generated SBOM contents.
+ *
+ * NOTE: These tests are only compatible with net6.0 and net472, as there are issues when resolving NuGet assemblies when
+ * targeting net8.0.
+ */
+ private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+
+ private static string projectDirectory = Path.Combine(Directory.GetCurrentDirectory(), "ProjectSamples", "ProjectSample1");
+ private static string sbomToolPath = Path.Combine(Directory.GetCurrentDirectory(), "sbom-tool");
+ private static string generateSbomTaskPath = Path.Combine(Directory.GetCurrentDirectory(), "Microsoft.Sbom.Targets.dll");
+
+ private static string sbomSpecificationName = "SPDX";
+ private static string sbomSpecificationVersion = "2.2";
+ private static string sbomSpecificationDirectoryName = $"{sbomSpecificationName}_{sbomSpecificationVersion}".ToLowerInvariant();
+ private string expectedPackageName;
+ private string expectedVersion;
+ private string expectedSupplier;
+ private string assemblyName;
+ private string expectedNamespace;
+ private string configuration;
+
+ [TestInitialize]
+ public void SetupLocator()
+ {
+ if (MSBuildLocator.CanRegister)
+ {
+ MSBuildLocator.RegisterDefaults();
+ }
+ }
+
+ [TestCleanup]
+ public void CleanOutputFolders()
+ {
+ var binDir = Path.Combine(projectDirectory, "bin");
+ var objDir = Path.Combine(projectDirectory, "obj");
+
+ try
+ {
+ if (Directory.Exists(binDir))
+ {
+ Directory.Delete(binDir, true);
+ }
+
+ if (Directory.Exists(objDir))
+ {
+ Directory.Delete(objDir, true);
+ }
+
+ ProjectCollection.GlobalProjectCollection.UnloadAllProjects();
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Failed to cleanup output directories. {ex}");
+ }
+ }
+
+ private Project SetupSampleProject()
+ {
+ // Create a Project object for ProjectSample1
+ var projectFile = Path.Combine(projectDirectory, "ProjectSample1.csproj");
+ var sampleProject = new Project(projectFile);
+
+ // Get all the expected default properties
+ SetDefaultProperties(sampleProject);
+
+ // Set the TargetFrameworks property to empty. By default, it sets this property to net6.0 and net8.0, which fails for net8.0 builds.
+ sampleProject.SetProperty("TargetFrameworks", string.Empty);
+
+ // Set the paths to the sbom-tool CLI tool and Microsoft.Sbom.Targets.dll
+ sampleProject.SetProperty("SbomToolBinaryOutputPath", sbomToolPath);
+ sampleProject.SetProperty("GenerateSbomTaskAssemblyFilePath", generateSbomTaskPath);
+
+ return sampleProject;
+ }
+
+ private void SetDefaultProperties(Project sampleProject)
+ {
+ expectedPackageName = sampleProject.GetPropertyValue("PackageId");
+ expectedVersion = sampleProject.GetPropertyValue("Version");
+ assemblyName = sampleProject.GetPropertyValue("AssemblyName");
+ configuration = sampleProject.GetPropertyValue("Configuration");
+
+ if (string.IsNullOrEmpty(expectedPackageName))
+ {
+ expectedPackageName = assemblyName;
+ }
+
+ if (string.IsNullOrEmpty(expectedPackageName))
+ {
+ expectedVersion = "1.0.0";
+ }
+ }
+
+ private void RestoreBuildPack(Project sampleProject)
+ {
+ var logger = new ConsoleLogger();
+
+ // Restore the project to create project.assets.json file
+ var restore = sampleProject.Build("Restore", new[] { logger });
+ Assert.IsTrue(restore, "Failed to restore the project");
+
+ // Next, build the project
+ var build = sampleProject.Build(logger);
+ Assert.IsTrue(build, "Failed to build the project");
+
+ // Finally, pack the project
+ var pack = sampleProject.Build("Pack", new[] { logger });
+ Assert.IsTrue(pack, "Failed to pack the project");
+ }
+
+ private void InspectPackageIsWellFormed(bool isManifestPathGenerated = true)
+ {
+ const string backSlash = "\\";
+ const string forwardSlash = "/";
+ // Unzip the contents of the NuGet package
+ var nupkgPath = Path.Combine(projectDirectory, "bin", configuration);
+ var nupkgFile = Path.Combine(nupkgPath, $"{expectedPackageName}.{expectedVersion}.nupkg");
+ var manifestRelativePath = Path.Combine("_manifest", sbomSpecificationDirectoryName, "manifest.spdx.json")
+ .Replace(backSlash, forwardSlash);
+
+ // Check the content of the NuGet package
+ using (var archive = ZipFile.Open(nupkgFile, ZipArchiveMode.Read))
+ {
+ Assert.IsTrue(archive.Entries.Count() > 0);
+ // Nuget's zip code expects forward slashes as path separators.
+ Assert.IsTrue(archive.Entries.All(entry => !entry.FullName.Contains(backSlash)));
+ Assert.AreEqual(isManifestPathGenerated, archive.Entries.Any(entry => entry.FullName.Equals(manifestRelativePath)));
+ }
+ }
+
+ [TestMethod]
+ public void SbomGenerationSucceedsForDefaultProperties()
+ {
+ if (!IsWindows)
+ {
+ Assert.Inconclusive("This test is not (yet) supported on non-Windows platforms.");
+ return;
+ }
+
+ // Create and setup a Project object for ProjectSample1
+ var sampleProject = SetupSampleProject();
+
+ // Restore, build, and pack the project
+ RestoreBuildPack(sampleProject);
+
+ // Extract the NuGet package
+ InspectPackageIsWellFormed();
+ }
+
+ [TestMethod]
+ public void SbomGenerationSucceedsForValidNamespaceBaseUriUniquePart()
+ {
+ if (!IsWindows)
+ {
+ Assert.Inconclusive("This test is not (yet) supported on non-Windows platforms.");
+ return;
+ }
+
+ // Create and setup a Project object for ProjectSample1
+ var sampleProject = SetupSampleProject();
+
+ // Manually set the NamespaceUriUniquePart
+ var namespaceUriUniquePart = Guid.NewGuid().ToString();
+ sampleProject.SetProperty("SbomGenerationNamespaceUriUniquePart", namespaceUriUniquePart);
+
+ // Restore, build, and pack the project
+ RestoreBuildPack(sampleProject);
+
+ // Extract the NuGet package
+ InspectPackageIsWellFormed();
+ }
+
+ [TestMethod]
+ public void SbomGenerationSucceedsForValidRequiredParams()
+ {
+ if (!IsWindows)
+ {
+ Assert.Inconclusive("This test is not (yet) supported on non-Windows platforms.");
+ return;
+ }
+
+ // Create and setup a Project object for ProjectSample1
+ var sampleProject = SetupSampleProject();
+
+ // Set require params
+ expectedPackageName = "SampleName";
+ expectedVersion = "3.2.5";
+ expectedSupplier = "SampleSupplier";
+ expectedNamespace = "https://example.com";
+
+ sampleProject.SetProperty("PackageId", expectedPackageName);
+ sampleProject.SetProperty("Version", expectedVersion);
+ sampleProject.SetProperty("SbomGenerationPackageName", expectedPackageName);
+ sampleProject.SetProperty("SbomGenerationPackageVersion", expectedVersion);
+ sampleProject.SetProperty("SbomGenerationPackageSupplier", expectedSupplier);
+ sampleProject.SetProperty("SbomGenerationNamespaceBaseUri", expectedNamespace);
+
+ // Restore, build, and pack the project
+ RestoreBuildPack(sampleProject);
+
+ // Extract the NuGet package
+ InspectPackageIsWellFormed();
+ }
+
+ [TestMethod]
+ public void SbomGenerationFailsForInvalidNamespaceUri()
+ {
+ if (!IsWindows)
+ {
+ Assert.Inconclusive("This test is not (yet) supported on non-Windows platforms.");
+ return;
+ }
+
+ // Create and setup a Project object for ProjectSample1
+ var sampleProject = SetupSampleProject();
+
+ // Set invalid namespace
+ expectedNamespace = "incorrect_uri";
+ sampleProject.SetProperty("SbomGenerationNamespaceBaseUri", expectedNamespace);
+
+ // Restore, build, and pack the project
+ var logger = new ConsoleLogger();
+
+ // Restore the project to create project.assets.json file
+ var restore = sampleProject.Build("Restore", new[] { logger });
+ Assert.IsTrue(restore, "Failed to restore the project");
+
+ // Next, build the project
+ var build = sampleProject.Build(logger);
+ Assert.IsTrue(build, "Failed to build the project");
+
+ // Ensure the packing step fails
+ var pack = sampleProject.Build("Pack", new[] { logger });
+ Assert.IsFalse(pack, "Packing succeeded when it should have failed");
+ }
+
+ [TestMethod]
+ public void SbomGenerationFailsForInvalidSupplierName()
+ {
+ if (!IsWindows)
+ {
+ Assert.Inconclusive("This test is not (yet) supported on non-Windows platforms.");
+ return;
+ }
+
+ // Create and setup a Project object for ProjectSample1
+ var sampleProject = SetupSampleProject();
+
+ // Set invalid supplier name
+ sampleProject.SetProperty("Authors", string.Empty);
+ sampleProject.SetProperty("AssemblyName", string.Empty);
+ sampleProject.SetProperty("SbomGenerationPackageSupplier", string.Empty);
+
+ // Restore, build, and pack the project
+ var logger = new ConsoleLogger();
+
+ // Restore the project to create project.assets.json file
+ var restore = sampleProject.Build("Restore", new[] { logger });
+ Assert.IsTrue(restore, "Failed to restore the project");
+
+ // Next, build the project
+ var build = sampleProject.Build(logger);
+ Assert.IsTrue(build, "Failed to build the project");
+
+ // Ensure the packing step fails
+ var pack = sampleProject.Build("Pack", new[] { logger });
+ Assert.IsFalse(pack, "Packing succeeded when it should have failed");
+ }
+
+ [TestMethod]
+ public void SbomGenerationSkipsForUnsetGenerateSBOMFlag()
+ {
+ if (!IsWindows)
+ {
+ Assert.Inconclusive("This test is not (yet) supported on non-Windows platforms.");
+ return;
+ }
+
+ // Create and setup a Project object for ProjectSample1
+ var sampleProject = SetupSampleProject();
+
+ // Set the GenerateSBOM property to empty.
+ sampleProject.SetProperty("GenerateSBOM", "false");
+
+ // Restore, build, and pack the project
+ RestoreBuildPack(sampleProject);
+
+ // Extract the NuGet package
+ InspectPackageIsWellFormed(isManifestPathGenerated: false);
+ }
+
+ [TestMethod]
+ public void SbomGenerationSucceedsForMultiTargetedProject()
+ {
+ if (!IsWindows)
+ {
+ Assert.Inconclusive("This test is not (yet) supported on non-Windows platforms.");
+ return;
+ }
+
+ // Create and setup a Project object for ProjectSample1
+ var sampleProject = SetupSampleProject();
+
+ // Set multi-target frameworks
+ sampleProject.SetProperty("TargetFramework", string.Empty);
+ sampleProject.SetProperty("TargetFrameworks", "net472;net6.0");
+
+ // Restore, build, and pack the project
+ RestoreBuildPack(sampleProject);
+
+ // Extract the NuGet package
+ InspectPackageIsWellFormed();
+ }
+}
diff --git a/test/Microsoft.Sbom.Targets.E2E.Tests/Microsoft.Sbom.Targets.E2E.Tests.csproj b/test/Microsoft.Sbom.Targets.E2E.Tests/Microsoft.Sbom.Targets.E2E.Tests.csproj
new file mode 100644
index 000000000..09b56f108
--- /dev/null
+++ b/test/Microsoft.Sbom.Targets.E2E.Tests/Microsoft.Sbom.Targets.E2E.Tests.csproj
@@ -0,0 +1,62 @@
+
+
+
+ net6.0;net472
+ true
+ false
+ True
+ Microsoft.Sbom.Targets.E2E.Tests
+ net6.0
+ $(MSBuildThisFileDirectory)..\..\src\Microsoft.Sbom.Tool\
+ $(MSBuildThisFileDirectory)..\..\src\Microsoft.Sbom.Targets\Microsoft.Sbom.Targets.targets
+
+
+
+ TRACE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_SbomToolFiles Include="$(SBOMCLIToolProjectDir)bin\$(Configuration)\$(SbomCLIToolTargetFramework)\publish\**\*.*">
+ false
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.Sbom.Targets.E2E.Tests/ProjectSamples/ProjectSample1/ProjectSample1.csproj b/test/Microsoft.Sbom.Targets.E2E.Tests/ProjectSamples/ProjectSample1/ProjectSample1.csproj
new file mode 100644
index 000000000..ab3494088
--- /dev/null
+++ b/test/Microsoft.Sbom.Targets.E2E.Tests/ProjectSamples/ProjectSample1/ProjectSample1.csproj
@@ -0,0 +1,21 @@
+
+
+ Library
+ true
+ net6.0
+ ProjectSample
+ 1.2.4
+ false
+ true
+ true
+ false
+
+
+
+
+ $(NoWarn);NU1507;NU5128
+
+
+
+
+
diff --git a/test/Microsoft.Sbom.Targets.E2E.Tests/ProjectSamples/ProjectSample1/SampleLibrary.cs b/test/Microsoft.Sbom.Targets.E2E.Tests/ProjectSamples/ProjectSample1/SampleLibrary.cs
new file mode 100644
index 000000000..053891714
--- /dev/null
+++ b/test/Microsoft.Sbom.Targets.E2E.Tests/ProjectSamples/ProjectSample1/SampleLibrary.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System;
+
+public class SampleLibrary
+{
+}
diff --git a/test/Microsoft.Sbom.Targets.Tests/AbstractGenerateSbomTaskInputTests.cs b/test/Microsoft.Sbom.Targets.Tests/AbstractGenerateSbomTaskInputTests.cs
new file mode 100644
index 000000000..5274dbf5d
--- /dev/null
+++ b/test/Microsoft.Sbom.Targets.Tests/AbstractGenerateSbomTaskInputTests.cs
@@ -0,0 +1,375 @@
+// 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.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using Microsoft.Build.Framework;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.Sbom.Targets.Tests;
+
+[TestClass]
+public abstract class AbstractGenerateSbomTaskInputTests
+{
+ internal abstract string SbomSpecification { get; }
+
+ internal static readonly string CurrentDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ internal static readonly string DefaultManifestDirectory = Path.Combine(CurrentDirectory, "_manifest");
+ internal static readonly string TemporaryDirectory = Path.Combine(CurrentDirectory, "_temporary");
+ internal static readonly string BuildComponentPath = Path.Combine(CurrentDirectory, "..", "..", "..");
+ internal static readonly string ExternalDocumentListFile = Path.GetRandomFileName();
+ internal static string SbomToolPath = Path.Combine(Directory.GetCurrentDirectory(), "sbom-tool");
+ internal const string PackageSupplier = "Test-Microsoft";
+ internal const string PackageName = "CoseSignTool";
+ internal const string PackageVersion = "0.0.1";
+ internal const string NamespaceBaseUri = "https://base0.uri";
+ private Mock buildEngine;
+ private List errors;
+ private List messages;
+
+ [TestInitialize]
+ public void Startup()
+ {
+ // Setup the build engine
+ this.buildEngine = new Mock();
+ this.errors = new List();
+ this.messages = new List();
+ this.buildEngine.Setup(x => x.LogErrorEvent(It.IsAny())).Callback(e => errors.Add(e));
+ this.buildEngine.Setup(x => x.LogMessageEvent(It.IsAny())).Callback(msg => messages.Add(msg));
+ }
+
+ [TestCleanup]
+ public void Cleanup() {
+ // Clean up the manifest directory
+ if (Directory.Exists(DefaultManifestDirectory))
+ {
+ Directory.Delete(DefaultManifestDirectory, true);
+ }
+
+ // Clean up the manifest directory
+ if (Directory.Exists(TemporaryDirectory))
+ {
+ Directory.Delete(TemporaryDirectory, true);
+ }
+ }
+
+ ///
+ /// Test for ensuring the GenerateSbom fails for null or empty inputs for
+ /// required params, which includes BuildDropPath, PackageSupplier, PackageName,
+ /// PackageVersion, and NamespaceBaseUri.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(GetNullRequiredParamsData), DynamicDataSourceType.Method)]
+ [DynamicData(nameof(GetEmptyRequiredParamsData), DynamicDataSourceType.Method)]
+ [DynamicData(nameof(GetWhiteSpace_Tabs_NewLineParamsData), DynamicDataSourceType.Method)]
+ public void Sbom_Fails_With_Null_Empty_And_WhiteSpace_Required_Params(
+ string buildDropPath,
+ string packageSupplier,
+ string packageName,
+ string packageVersion,
+ string namespaceBaseUri,
+ string sbomToolPath)
+ {
+ // Arrange.
+ var task = new GenerateSbom
+ {
+ BuildDropPath = buildDropPath,
+ PackageSupplier = packageSupplier,
+ PackageName = packageName,
+ PackageVersion = packageVersion,
+ NamespaceBaseUri = namespaceBaseUri,
+ ManifestInfo = this.SbomSpecification,
+ BuildEngine = this.buildEngine.Object,
+#if NET472
+ SbomToolPath = sbomToolPath,
+#endif
+ };
+
+ // Act
+ var result = task.Execute();
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ private static IEnumerable