From 43e316df9dde0286bdd76f64eae82fbaf9a69aef Mon Sep 17 00:00:00 2001 From: Pragnya <59893188+pragnya17@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:38:15 -0800 Subject: [PATCH] Defining and generating spdx 3.0 json elements (#830) * spdx 3.0 generator changes + unit tests * address PR review comments * fix bug in UT + address PR comments * remove empty constructors * add debug line * fix UT with regex * remove extra debug line --------- Co-authored-by: ppandrate --- Microsoft.Sbom.sln | 12 + src/Microsoft.Sbom.Common/GeneratorUtils.cs | 40 ++ ...ernalMetadataProviderIdentityExtensions.cs | 47 +- .../Microsoft.Sbom.Common.csproj | 1 - .../Generator.cs | 73 +- ...osoft.Sbom.Parsers.Spdx22SbomParser.csproj | 1 + .../Constants.cs | 37 ++ .../Entities/AnyLicenseInfo.cs | 18 + .../Entities/ContentIdentifier.cs | 30 + .../Entities/CreationInfo.cs | 53 ++ .../Entities/Element.cs | 71 ++ .../Entities/Enums/HashAlgorithm.cs | 41 ++ .../Entities/Enums/ProfileIdentifierType.cs | 30 + .../Entities/Enums/RelationshipType.cs | 316 +++++++++ .../Entities/ExternalIdentifier.cs | 40 ++ .../Entities/ExternalMap.cs | 34 + .../Entities/File.cs | 25 + .../Entities/FormatEnforcedSPDX3.cs | 43 ++ .../Entities/NamespaceMap.cs | 16 + .../Entities/NoAssertionElement.cs | 18 + .../Entities/NoneElement.cs | 18 + .../Entities/Organization.cs | 13 + .../Entities/Package.cs | 26 + .../Entities/PackageVerificationCode.cs | 33 + .../Entities/Person.cs | 13 + .../Entities/Snippet.cs | 15 + .../Entities/Software.cs | 82 +++ .../Entities/Spdx30Relationship.cs | 55 ++ .../Entities/SpdxDocument.cs | 39 ++ .../Entities/Tool.cs | 14 + .../Exceptions/MissingHashValueException.cs | 24 + .../Generator.cs | 629 ++++++++++++++++++ ...osoft.Sbom.Parsers.Spdx30SbomParser.csproj | 26 + .../Utils/ElementSerializer.cs | 29 + .../Utils/SPDXExtensions.cs | 173 +++++ ...MetadataProviderIdentityExtensionsTests.cs | 1 + ...Sbom.Parsers.Spdx30SbomParser.Tests.csproj | 17 + .../Parser/GeneratorTests.cs | 237 +++++++ .../JsonStrings/SbomDocCreationJsonStrings.cs | 74 +++ .../JsonStrings/SbomExternalMapJsonStrings.cs | 26 + .../Parser/JsonStrings/SbomFileJsonStrings.cs | 61 ++ .../JsonStrings/SbomPackageJsonStrings.cs | 134 ++++ .../SbomRelationshipJsonStrings.cs | 22 + 43 files changed, 2636 insertions(+), 71 deletions(-) create mode 100644 src/Microsoft.Sbom.Common/GeneratorUtils.cs rename src/{Microsoft.Sbom.Parsers.Spdx22SbomParser/Utils => Microsoft.Sbom.Common}/InternalMetadataProviderIdentityExtensions.cs (75%) create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Constants.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/AnyLicenseInfo.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ContentIdentifier.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/CreationInfo.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Element.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/HashAlgorithm.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/ProfileIdentifierType.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/RelationshipType.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ExternalIdentifier.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ExternalMap.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/File.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/FormatEnforcedSPDX3.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NamespaceMap.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NoAssertionElement.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NoneElement.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Organization.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Package.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/PackageVerificationCode.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Person.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Snippet.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Software.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Spdx30Relationship.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/SpdxDocument.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Tool.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Exceptions/MissingHashValueException.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Generator.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Microsoft.Sbom.Parsers.Spdx30SbomParser.csproj create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/ElementSerializer.cs create mode 100644 src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/SPDXExtensions.cs create mode 100644 test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests.csproj create mode 100644 test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/GeneratorTests.cs create mode 100644 test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomDocCreationJsonStrings.cs create mode 100644 test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomExternalMapJsonStrings.cs create mode 100644 test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFileJsonStrings.cs create mode 100644 test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomPackageJsonStrings.cs create mode 100644 test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomRelationshipJsonStrings.cs diff --git a/Microsoft.Sbom.sln b/Microsoft.Sbom.sln index 643283e9..cd1421e7 100644 --- a/Microsoft.Sbom.sln +++ b/Microsoft.Sbom.sln @@ -57,6 +57,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Sbom.Tool.Tests", 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Sbom.Parsers.Spdx30SbomParser", "src\Microsoft.Sbom.Parsers.Spdx30SbomParser\Microsoft.Sbom.Parsers.Spdx30SbomParser.csproj", "{476B9C87-293F-4BF7-B39A-EB732083409F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests", "test\Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests\Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests.csproj", "{E3FE33BB-FAB2-4F60-B930-BEB736AACE25}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -131,6 +135,14 @@ Global {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 + {476B9C87-293F-4BF7-B39A-EB732083409F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {476B9C87-293F-4BF7-B39A-EB732083409F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {476B9C87-293F-4BF7-B39A-EB732083409F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {476B9C87-293F-4BF7-B39A-EB732083409F}.Release|Any CPU.Build.0 = Release|Any CPU + {E3FE33BB-FAB2-4F60-B930-BEB736AACE25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3FE33BB-FAB2-4F60-B930-BEB736AACE25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3FE33BB-FAB2-4F60-B930-BEB736AACE25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3FE33BB-FAB2-4F60-B930-BEB736AACE25}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Microsoft.Sbom.Common/GeneratorUtils.cs b/src/Microsoft.Sbom.Common/GeneratorUtils.cs new file mode 100644 index 00000000..0f093d3e --- /dev/null +++ b/src/Microsoft.Sbom.Common/GeneratorUtils.cs @@ -0,0 +1,40 @@ +// 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.Common; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Sbom.Contracts; +using Microsoft.Sbom.Contracts.Enums; +using Microsoft.Sbom.Extensions.Exceptions; + +/// +/// A class for methods that are used by SPDX generators, regardless of which SPDX version is being used. +/// +public class GeneratorUtils +{ + // Throws a if the filehashes are missing + // any of the required hashes + public static void EnsureRequiredHashesPresent(Checksum[] fileHashes, AlgorithmName[] requiredHashAlgorithms) + { + foreach (var hashAlgorithmName in from hashAlgorithmName in requiredHashAlgorithms + where !fileHashes.Select(fh => fh.Algorithm).Contains(hashAlgorithmName) + select hashAlgorithmName) + { + throw new MissingHashValueException($"The hash value for algorithm {hashAlgorithmName} is missing from {nameof(fileHashes)}"); + } + } + + public static string EnsureRelativePathStartsWithDot(string path) + { + if (!path.StartsWith(".", StringComparison.Ordinal)) + { + return "." + path; + } + + return path; + } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Utils/InternalMetadataProviderIdentityExtensions.cs b/src/Microsoft.Sbom.Common/InternalMetadataProviderIdentityExtensions.cs similarity index 75% rename from src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Utils/InternalMetadataProviderIdentityExtensions.cs rename to src/Microsoft.Sbom.Common/InternalMetadataProviderIdentityExtensions.cs index d0e999b9..a5ffee73 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Utils/InternalMetadataProviderIdentityExtensions.cs +++ b/src/Microsoft.Sbom.Common/InternalMetadataProviderIdentityExtensions.cs @@ -8,10 +8,9 @@ using System.Text; using Microsoft.Sbom.Extensions; using Microsoft.Sbom.Extensions.Entities; -using Microsoft.Sbom.Parsers.Spdx22SbomParser.Entities; using HashAlgorithmName = Microsoft.Sbom.Contracts.Enums.AlgorithmName; -namespace Microsoft.Sbom.Parsers.Spdx22SbomParser.Utils; +namespace Microsoft.Sbom.Common; /// /// Provides helper functions to generate identity strings for SPDX. @@ -51,59 +50,19 @@ public static string GetPackageName(this IInternalMetadataProvider internalMetad $"Please provide the package name in the 'PackageName' parameter."); } - /// - /// Generates the package verification code for a given package using the SPDX 2.2 specification. - /// - /// Algorithm defined here https://spdx.github.io/spdx-spec/v2.2.2/package-information/#79-package-verification-code-field. - /// - /// - /// - public static PackageVerificationCode GetPackageVerificationCode(this IInternalMetadataProvider internalMetadataProvider) - { - if (internalMetadataProvider is null) - { - throw new ArgumentNullException(nameof(internalMetadataProvider)); - } - - // Get a list of SHA1 checksums - IList sha1Checksums = new List(); - foreach (var checksumArray in internalMetadataProvider.GetGenerationData(Constants.Spdx22ManifestInfo).Checksums) - { - sha1Checksums.Add(checksumArray - .Where(c => c.Algorithm == HashAlgorithmName.SHA1) - .Select(c => c.ChecksumValue) - .FirstOrDefault()); - } - - var packageChecksumString = string.Join(string.Empty, sha1Checksums.OrderBy(s => s)); -#pragma warning disable CA5350 // Suppress Do Not Use Weak Cryptographic Algorithms as we use SHA1 intentionally - var sha1Hasher = SHA1.Create(); -#pragma warning restore CA5350 - var hashByteArray = sha1Hasher.ComputeHash(Encoding.Default.GetBytes(packageChecksumString)); - - return new PackageVerificationCode - { - PackageVerificationCodeValue = BitConverter - .ToString(hashByteArray) - .Replace("-", string.Empty) - .ToLowerInvariant(), - PackageVerificationCodeExcludedFiles = null // We currently don't ignore any files. - }; - } - /// /// Gets a list of file ids that are included in this package. /// /// /// - public static List GetPackageFilesList(this IInternalMetadataProvider internalMetadataProvider) + public static List GetPackageFilesList(this IInternalMetadataProvider internalMetadataProvider, ManifestInfo manifestInfo) { if (internalMetadataProvider is null) { throw new ArgumentNullException(nameof(internalMetadataProvider)); } - return internalMetadataProvider.GetGenerationData(Constants.Spdx22ManifestInfo).FileIds.ToList(); + return internalMetadataProvider.GetGenerationData(manifestInfo).FileIds.ToList(); } /// diff --git a/src/Microsoft.Sbom.Common/Microsoft.Sbom.Common.csproj b/src/Microsoft.Sbom.Common/Microsoft.Sbom.Common.csproj index 818e9aec..0c4e6b62 100644 --- a/src/Microsoft.Sbom.Common/Microsoft.Sbom.Common.csproj +++ b/src/Microsoft.Sbom.Common/Microsoft.Sbom.Common.csproj @@ -17,5 +17,4 @@ - diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Generator.cs b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Generator.cs index e93fad2e..b9377639 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Generator.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Generator.cs @@ -4,7 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; +using Microsoft.Sbom.Common; using Microsoft.Sbom.Contracts; using Microsoft.Sbom.Contracts.Enums; using Microsoft.Sbom.Extensions; @@ -68,7 +71,7 @@ private SPDXFile ConvertSbomFileToSpdxFile(InternalSbomFileInfo fileInfo) throw new ArgumentException(nameof(fileInfo.Path)); } - EnsureRequiredHashesPresent(fileInfo.Checksum.ToArray()); + GeneratorUtils.EnsureRequiredHashesPresent(fileInfo.Checksum.ToArray(), RequiredHashAlgorithms); var spdxFileElement = new SPDXFile { @@ -76,7 +79,7 @@ private SPDXFile ConvertSbomFileToSpdxFile(InternalSbomFileInfo fileInfo) .Where(c => RequiredHashAlgorithms.Contains(c.Algorithm)) .Select(fh => new Entities.Checksum { Algorithm = fh.Algorithm.ToString(), ChecksumValue = fh.ChecksumValue.ToLower() }) .ToList(), - FileName = EnsureRelativePathStartsWithDot(fileInfo.Path), + FileName = GeneratorUtils.EnsureRelativePathStartsWithDot(fileInfo.Path), FileCopyrightText = fileInfo.FileCopyrightText ?? Constants.NoAssertionValue, LicenseConcluded = fileInfo.LicenseConcluded ?? Constants.NoAssertionValue, LicenseInfoInFiles = fileInfo.LicenseInfoInFiles ?? Constants.NoAssertionListValue, @@ -87,18 +90,6 @@ private SPDXFile ConvertSbomFileToSpdxFile(InternalSbomFileInfo fileInfo) return spdxFileElement; } - // Throws a if the filehashes are missing - // any of the required hashes - private void EnsureRequiredHashesPresent(Contracts.Checksum[] fileHashes) - { - foreach (var hashAlgorithmName in from hashAlgorithmName in RequiredHashAlgorithms - where !fileHashes.Select(fh => fh.Algorithm).Contains(hashAlgorithmName) - select hashAlgorithmName) - { - throw new MissingHashValueException($"The hash value for algorithm {hashAlgorithmName} is missing from {nameof(fileHashes)}"); - } - } - public ManifestInfo RegisterManifest() => Constants.Spdx22ManifestInfo; public IDictionary GetMetadataDictionary(IInternalMetadataProvider internalMetadataProvider) @@ -184,16 +175,6 @@ public GenerationResult GenerateJsonDocument(SbomPackage packageInfo) }; } - private string EnsureRelativePathStartsWithDot(string path) - { - if (!path.StartsWith(".", StringComparison.Ordinal)) - { - return "." + path; - } - - return path; - } - public GenerationResult GenerateRootPackage( IInternalMetadataProvider internalMetadataProvider) { @@ -223,9 +204,9 @@ public GenerationResult GenerateRootPackage( LicenseDeclared = Constants.NoAssertionValue, LicenseInfoFromFiles = Constants.NoAssertionListValue, FilesAnalyzed = true, - PackageVerificationCode = internalMetadataProvider.GetPackageVerificationCode(), + PackageVerificationCode = GetPackageVerificationCode(internalMetadataProvider), Supplier = string.Format(Constants.PackageSupplierFormatString, internalMetadataProvider.GetPackageSupplier()), - HasFiles = internalMetadataProvider.GetPackageFilesList() + HasFiles = internalMetadataProvider.GetPackageFilesList(Constants.Spdx22ManifestInfo) }; return new GenerationResult @@ -331,4 +312,44 @@ public GenerationResult GenerateJsonDocument(ExternalDocumentReferenceInfo exter } }; } + + /// + /// Generates the package verification code for a given package using the SPDX 2.2 specification. + /// + /// Algorithm defined here https://spdx.github.io/spdx-spec/v2.2.2/package-information/#79-package-verification-code-field. + /// + /// + /// + private PackageVerificationCode GetPackageVerificationCode(IInternalMetadataProvider internalMetadataProvider) + { + if (internalMetadataProvider is null) + { + throw new ArgumentNullException(nameof(internalMetadataProvider)); + } + + // Get a list of SHA1 checksums + IList sha1Checksums = new List(); + foreach (var checksumArray in internalMetadataProvider.GetGenerationData(Constants.Spdx22ManifestInfo).Checksums) + { + sha1Checksums.Add(checksumArray + .Where(c => c.Algorithm == AlgorithmName.SHA1) + .Select(c => c.ChecksumValue) + .FirstOrDefault()); + } + + var packageChecksumString = string.Join(string.Empty, sha1Checksums.OrderBy(s => s)); +#pragma warning disable CA5350 // Suppress Do Not Use Weak Cryptographic Algorithms as we use SHA1 intentionally + var sha1Hasher = SHA1.Create(); +#pragma warning restore CA5350 + var hashByteArray = sha1Hasher.ComputeHash(Encoding.Default.GetBytes(packageChecksumString)); + + return new PackageVerificationCode + { + PackageVerificationCodeValue = BitConverter + .ToString(hashByteArray) + .Replace("-", string.Empty) + .ToLowerInvariant(), + PackageVerificationCodeExcludedFiles = null // We currently don't ignore any files. + }; + } } diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Microsoft.Sbom.Parsers.Spdx22SbomParser.csproj b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Microsoft.Sbom.Parsers.Spdx22SbomParser.csproj index b9394b9f..47e3b409 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Microsoft.Sbom.Parsers.Spdx22SbomParser.csproj +++ b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Microsoft.Sbom.Parsers.Spdx22SbomParser.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Constants.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Constants.cs new file mode 100644 index 00000000..6b460cb6 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Constants.cs @@ -0,0 +1,37 @@ +// 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 Microsoft.Sbom.Extensions.Entities; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser; + +internal static class Constants +{ + internal const string SPDXName = "SPDX"; + internal const string SPDXVersion = "3.0"; + internal const string DataLicenceValue = "CC0-1.0"; + internal const string SPDXDocumentIdValue = "SPDXRef-DOCUMENT"; + internal const string RootPackageIdValue = "SPDXRef-RootPackage"; + internal const string SPDXDocumentNameFormatString = "{0} {1}"; + internal const string PackageSupplierFormatString = "Organization: {0}"; + + /// + /// Use if SPDX creator + /// - made an attempt to retrieve the info but cannot determine correct values. + /// - made no attempt to retrieve the info. + /// - has intentionally provided no information. + /// + internal const string NoAssertionValue = "NOASSERTION"; + + /// + /// The value as a list with a single item. + /// + internal static IEnumerable NoAssertionListValue = new List { NoAssertionValue }; + + internal static ManifestInfo Spdx30ManifestInfo = new ManifestInfo + { + Name = SPDXName, + Version = SPDXVersion + }; +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/AnyLicenseInfo.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/AnyLicenseInfo.cs new file mode 100644 index 00000000..07cf6667 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/AnyLicenseInfo.cs @@ -0,0 +1,18 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// Class defintion is based on: https://spdx.github.io/spdx-spec/v3.0.1/model/SimpleLicensing/Classes/AnyLicenseInfo/ +/// +public class AnyLicenseInfo : Element +{ + public AnyLicenseInfo() + { + SpdxId = "SPDXRef-AnyLicenseInfo"; + } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ContentIdentifier.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ContentIdentifier.cs new file mode 100644 index 00000000..d6e76f16 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ContentIdentifier.cs @@ -0,0 +1,30 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// A ContentIdentifier is a canonical, unique, immutable identifier of the content of a software artifact, such as a package, a file, or a snippet. +/// It can be used for verifying its identity and integrity. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/ContentIdentifier/ +/// +public class ContentIdentifier : Software +{ + private string contentIdentifierType; + + /// + /// Gets or sets the content identifier type. + /// Allowed types are Git Object ID and Software Hash Identifier (swhid). + /// We will use swhid unless otherwise specified. + /// + [JsonRequired] + [JsonPropertyName("contentIdentifierType")] + public override string ContentIdentifierType + { + get => this.contentIdentifierType ?? "swhid"; + set => this.contentIdentifierType = value; + } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/CreationInfo.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/CreationInfo.cs new file mode 100644 index 00000000..e066aff8 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/CreationInfo.cs @@ -0,0 +1,53 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// Used to define creation information about the SPDX element. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/CreationInfo/ +/// +public class CreationInfo : Element +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("@id")] + public string Id { get; set; } + + /// + /// Gets or sets a string that specifies the time the SBOM was created on. + /// + [JsonRequired] + [JsonPropertyName("created")] + public string Created { get; set; } + + /// + /// Gets or sets a list of strings that specify metadata about the creators of this SBOM. + /// This could be a person, organization, software agent, etc. and is represented by the Agent class. + /// This is not to be confused with tools that are used to perform tasks. + /// + [JsonRequired] + [JsonPropertyName("createdBy")] + public IEnumerable CreatedBy { get; set; } + + /// + /// Gets or sets a list of strings that specify metadata about the tools used to create this SBOM. + /// A tool is an element of hardware and/or software utilized to carry out a particular function. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("createdUsing")] + public IEnumerable CreatedUsing { get; set; } + + [JsonRequired] + [JsonPropertyName("specVersion")] + public string SpecVersion { get; set; } + + /// + /// Make sure that creation info details are not serialized/deserialized when creating a CreationInfo element. + /// CreationInfoDetails is a property in the base class Element that is inherited by all other classes except for CreationInfo. + /// + [JsonIgnore] + public new string CreationInfoDetails { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Element.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Element.cs new file mode 100644 index 00000000..8c33f930 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Element.cs @@ -0,0 +1,71 @@ +// 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; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// Base domain class from which all other SPDX-3.0 domain classes derive. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Element/ +/// +public abstract class Element +{ + protected Element() + { + CreationInfoDetails = "_:creationinfo"; + Type = GetType().Name; + } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("comment")] + public string Comment { get; set; } + + [JsonRequired] + [JsonPropertyName("creationInfo")] + public string CreationInfoDetails { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("extension")] + public List Extension { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("externalIdentifier")] + public List ExternalIdentifier { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("externalRef")] + public List ExternalRef { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets unique Identifier for elements in SPDX document. + /// + [JsonRequired] + [JsonPropertyName("spdxId")] + public string SpdxId { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("summary")] + public string Summary { get; set; } + + /// + /// Gets or sets on how packages were verified. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("verifiedUsing")] + public List VerifiedUsing { get; set; } + + [JsonRequired] + [JsonPropertyName("type")] + public string Type { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/HashAlgorithm.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/HashAlgorithm.cs new file mode 100644 index 00000000..64c741aa --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/HashAlgorithm.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums; + +/// +/// Defined hash algorithms: https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Vocabularies/HashAlgorithm/ +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1300:Element should begin with upper-case letter", + Justification = "These are enum types that are case sensitive and defined by external code.")] +public enum HashAlgorithm +{ + adler32, + blake2b256, + blake2b384, + blake2b512, + blake3, + crystalsDilithium, + crystalsKyber, + falcon, + md2, + md4, + md5, + md6, + other, + sha1, + sha224, + sha256, + sha384, + sha3_224, + sha3_256, + sha3_384, + sha3_512, + sha512 +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/ProfileIdentifierType.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/ProfileIdentifierType.cs new file mode 100644 index 00000000..c840d327 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/ProfileIdentifierType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums; + +/// +/// There are a set of profiles that have been defined by a profile team. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Vocabularies/ProfileIdentifierType/. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1300:Element should begin with upper-case letter", + Justification = "These are enum types that are case sensitive and defined by external code.")] +public enum ProfileIdentifierType +{ + ai, + build, + core, + dataset, + expandedLicensing, + extension, + lite, + security, + simpleLicensing, + software +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/RelationshipType.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/RelationshipType.cs new file mode 100644 index 00000000..ce212716 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/RelationshipType.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums; + +/// +/// Defines the type of between the source and the target element. +/// Full definition here: https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Vocabularies/RelationshipType/ +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum RelationshipType +{ + /// + /// The from Vulnerability affects each to Element. + /// + AFFECTS, + + /// + /// The from Element is amended by each to Element. + /// + AMENDED_BY, + + /// + /// The from Element is an ancestor of each to Element. + /// + ANCESTOR_OF, + + /// + /// The from Element is available from the additional supplier described by each to Element. + /// + AVAILABLE_FROM, + + /// + /// The from Element is a configuration applied to each to Element, during a LifecycleScopeType period. + /// + CONFIGURES, + + /// + /// The from Element contains each to Element. + /// + CONTAINS, + + /// + /// The from Vulnerability is coordinatedBy the to Agent(s) (vendor, researcher, or consumer agent). + /// + COORDINATED_BY, + + /// + /// The from Element has been copied to each to Element. + /// + COPIED_TO, + + /// + /// The from Agent is delegating an action to the Agent of the to Relationship (which must be of type invokedBy), + /// during a LifecycleScopeType (e.g. the to invokedBy Relationship is being done on behalf of from). + /// + DELEGATED_TO, + + /// + /// The from Element depends on each to Element, during a LifecycleScopeType period. + /// + DEPENDS_ON, + + /// + /// The from Element is a descendant of each to Element. + /// + DESCENDANT_OF, + + /// + /// The from Element describes each to Element. To denote the root(s) of a tree of elements in a collection, the rootElement property should be used. + /// + DESCRIBES, + + /// + /// The from Vulnerability has no impact on each to Element. The use of the doesNotAffect is constrained to VexNotAffectedVulnAssessmentRelationship classed relationships. + /// + DOES_NOT_AFFECT, + + /// + /// The from archive expands out as an artifact described by each to Element. + /// + EXPANDS_TO, + + /// + /// The from Vulnerability has had an exploit created against it by each to Agent. + /// + EXPLOIT_CREATED_BY, + + /// + /// Designates a from Vulnerability has been fixed by the to Agent(s). + /// + FIXED_BY, + + /// + /// A from Vulnerability has been fixed in each to Element. The use of the fixedIn type is constrained to VexFixedVulnAssessmentRelationship classed relationships. + /// + FIXED_IN, + + /// + /// Designates a from Vulnerability was originally discovered by the to Agent(s). + /// + FOUND_BY, + + /// + /// The from Element generates each to Element. + /// + GENERATES, + + /// + /// Every to Element is a file added to the from Element (from hasAddedFile to). + /// + HAS_ADDED_FILE, + + /// + /// Relates a from Vulnerability and each to Element with a security assessment. To be used with VulnAssessmentRelationship types. + /// + HAS_ASSESSMENT_FOR, + + /// + /// Used to associate a from Artifact with each to Vulnerability. + /// + HAS_ASSOCIATED_VULNERABILITY, + + /// + /// The from SoftwareArtifact is concluded by the SPDX data creator to be governed by each to license. + /// + HAS_CONCLUDED_LICENSE, + + /// + /// The from Element treats each to Element as a data file. + /// A data file is an artifact that stores data required or optional for the from Element's functionality. + /// A data file can be a database file, an index file, a log file, an AI model file, a calibration data file, a temporary file, a backup file, and more. + /// For AI training dataset, test dataset, test artifact, configuration data, build input data, and build output data, + /// please consider using the more specific relationship types: trainedOn, testedOn, hasTest, configures, hasInput, and hasOutput, respectively. + /// This relationship does not imply dependency. + /// + HAS_DATA_FILE, + + /// + /// The from SoftwareArtifact was discovered to actually contain each to license, for example as detected by use of automated tooling. + /// + HAS_DECLARED_LICENSE, + + /// + /// Every to Element is a file deleted from the from Element (from hasDeletedFile to). + /// + HAS_DELETED_FILE, + + /// + /// The from Element has manifest files that contain dependency information in each to Element. + /// + HAS_DEPENDENCY_MANIFEST, + + /// + /// The from Element is distributed as an artifact in each to Element (e.g. an RPM or archive file). + /// + HAS_DISTRIBUTION_ARTIFACT, + + /// + /// The from Element is documented by each to Element. + /// + HAS_DOCUMENTATION, + + /// + /// The from Element dynamically links in each to Element, during a LifecycleScopeType period. + /// + HAS_DYNAMIC_LINK, + + /// + /// Every to Element is considered as evidence for the from Element (from hasEvidence to). + /// + HAS_EVIDENCE, + + /// + /// Every to Element is an example for the from Element (from hasExample to). + /// + HAS_EXAMPLE, + + /// + /// The from Build was run on the to Element during a LifecycleScopeType period (e.g. the host that the build runs on). + /// + HAS_HOST, + + /// + /// The from Build has each to Element as an input, during a LifecycleScopeType period. + /// + HAS_INPUT, + + /// + /// Every to Element is metadata about the from Element (from hasMetadata to). + /// + HAS_METADATA, + + /// + /// Every to Element is an optional component of the from Element (from hasOptionalComponent to). + /// + HAS_OPTIONAL_COMPONENT, + + /// + /// The from Element optionally depends on each to Element, during a LifecycleScopeType period. + /// + HAS_OPTIONAL_DEPENDENCY, + + /// + /// The from Build element generates each to Element as an output, during a LifecycleScopeType period. + /// + HAS_OUTPUT, + + /// + /// The from Element has a prerequisite on each to Element, during a LifecycleScopeType period. + /// + HAS_PREREQUISITE, + + /// + /// The from Element has a dependency on each to Element, dependency is not in the distributed artifact, but assumed to be provided, during a LifecycleScopeType period. + /// + HAS_PROVIDED_DEPENDENCY, + + /// + /// The from Element has a requirement on each to Element, during a LifecycleScopeType period. + /// + HAS_REQUIREMENT, + + /// + /// Every to Element is a specification for the from Element (from hasSpecification to), during a LifecycleScopeType period. + /// + HAS_SPECIFICATION, + + /// + /// The from Element statically links in each to Element, during a LifecycleScopeType period. + /// + HAS_STATIC_LINK, + + /// + /// Every to Element is a test artifact for the from Element (from hasTest to), during a LifecycleScopeType period. + /// + HAS_TEST, + + /// + /// Every to Element is a test case for the from Element (from hasTestCase to). + /// + HAS_TEST_CASE, + + /// + /// Every to Element is a variant the from Element (from hasVariant to). + /// + HAS_VARIANT, + + /// + /// The from Element was invoked by the to Agent, during a LifecycleScopeType period (for example, a Build element that describes a build step). + /// + INVOKED_BY, + + /// + /// The from Element is modified by each to Element. + /// + MODIFIED_BY, + + /// + /// Every to Element is related to the from Element where the relationship type is not described by any of the SPDX relationship types (this relationship is directionless). + /// + OTHER, + + /// + /// Every to Element is a packaged instance of the from Element (from packagedBy to). + /// + PACKAGED_BY, + + /// + /// Every to Element is a patch for the from Element (from patchedBy to). + /// + PATCHED_BY, + + /// + /// Designates a from Vulnerability was made available for public use or reference by each to Agent. + /// + PUBLISHED_BY, + + /// + /// Designates a from Vulnerability was first reported to a project, vendor, or tracking database for formal identification by each to Agent. + /// + REPORTED_BY, + + /// + /// Designates a from Vulnerability's details were tracked, aggregated, and/or enriched to improve context (i.e. NVD) by each to Agent. + /// + REPUBLISHED_BY, + + /// + /// The from SpdxDocument can be found in a serialized form in each to Artifact. + /// + SERIALIZED_IN_ARTIFACT, + + /// + /// The from Element has been tested on the to Element(s). + /// + TESTED_ON, + + /// + /// The from Element has been trained on the to Element(s). + /// + TRAINED_ON, + + /// + /// The from Vulnerability impact is being investigated for each to Element. + /// The use of the underInvestigationFor type is constrained to VexUnderInvestigationVulnAssessmentRelationship classed relationships. + /// + UNDER_INVESTIGATION_FOR, + + /// + /// The from Element uses each to Element as a tool, during a LifecycleScopeType period. + /// + USES_TOOL +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ExternalIdentifier.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ExternalIdentifier.cs new file mode 100644 index 00000000..41846e0a --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ExternalIdentifier.cs @@ -0,0 +1,40 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// An ExternalIdentifier is a reference to a resource outside the scope of SPDX-3.0 content +/// that provides a unique key within an established domain that can uniquely identify an Element. +/// This is the equivalent of ExternalRef in SPDX-2.2. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/ExternalIdentifier/ +/// +public class ExternalIdentifier : Element +{ + /// + /// Gets or sets type of the external identifier. + /// + [JsonRequired] + [JsonPropertyName("externalIdentifierType")] + public string ExternalIdentifierType { get; set; } + + [JsonRequired] + [JsonPropertyName("identifier")] + public string Identifier { get; set; } + + /// + /// Gets or sets a unique string without any spaces that specifies a location where the package specific information + /// can be located. This will be in the form of a uri. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("identifierLocator")] + public List IdentifierLocator { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("issuingAuthority")] + public string IssuingAuthority { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ExternalMap.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ExternalMap.cs new file mode 100644 index 00000000..67ad0dab --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/ExternalMap.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// SPDX 3.0 format External Map (equivalent of externalDocumentRef in SPDX 2.2). +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/ExternalMap/ +/// +public class ExternalMap : Element +{ + /// + /// This will not be used in the actual SBOM generation, therefore deserialization/serialization to a specific type is not required. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("definingArtifact")] + public object DefiningArtifact { get; set; } + + /// + /// Gets or sets unique Identifier for ExternalMap in SPDX document. + /// + [JsonRequired] + [JsonPropertyName("externalSpdxId")] + public string ExternalSpdxId { get; set; } + + /// + /// Gets or sets url value of package location. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("locationHint")] + public string LocationHint { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/File.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/File.cs new file mode 100644 index 00000000..6b03da44 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/File.cs @@ -0,0 +1,25 @@ +// 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; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// Refers to any object that stores content on a computer. +/// The type of content can optionally be provided in the contentType property. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/File/ +/// +public class File : Software +{ + public File() + { + Type = "software_File"; + } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mediaType")] + public object MediaType { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/FormatEnforcedSPDX3.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/FormatEnforcedSPDX3.cs new file mode 100644 index 00000000..3a7b1466 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/FormatEnforcedSPDX3.cs @@ -0,0 +1,43 @@ +// 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.Parsers.Spdx30SbomParser.Entities; + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +// This class uses JSON serialization attributes to enforce the SPDX 3.x format +// Metadata fields tagged as required are required by the SPDX 3.x specification. +// SPDX 3.x specification link, only the Core Profile is mandatory: https://spdx.github.io/spdx-spec/v3.0.1/conformance/ +// The SPDX 3.x documents also have to be a strict subset of JSON-LD https://json-ld.org/ +public class FormatEnforcedSPDX3 +{ + /// + /// Indicates that this is an SPDX document, and this provided URL tells us how to decode it. + /// A requirement for JSON-LD documents. + /// + [JsonRequired] + [JsonPropertyName("@context")] + public string Context { get; set; } + + /// + /// A requirement for JSON-LD documents. + /// + [JsonRequired] + [JsonPropertyName("@graph")] + public List Graph { get; set; } + + /// + /// Required as part of the Core profile. + /// + [JsonRequired] + [JsonPropertyName("spdxId")] + public string SpdxId { get; set; } + + /// + /// Required as part of the Core profile. + /// + [JsonRequired] + [JsonPropertyName("creationInfo")] + public CreationInfo CreationInfo { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NamespaceMap.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NamespaceMap.cs new file mode 100644 index 00000000..eb53a324 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NamespaceMap.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.Parsers.Spdx30SbomParser.Entities; + +using System.Text.Json.Serialization; + +/// +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/NamespaceMap/ +/// +public class NamespaceMap : Element +{ + [JsonRequired] + [JsonPropertyName("namespace")] + public string Namespace { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NoAssertionElement.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NoAssertionElement.cs new file mode 100644 index 00000000..d1dbbcde --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NoAssertionElement.cs @@ -0,0 +1,18 @@ +// 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; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +public class NoAssertionElement : Element +{ + public NoAssertionElement() + { + Name = "NoAssertion"; + SpdxId = "SPDXRef-NoAssertion"; + Type = nameof(Element); + } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NoneElement.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NoneElement.cs new file mode 100644 index 00000000..f0f7a7b2 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NoneElement.cs @@ -0,0 +1,18 @@ +// 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; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +public class NoneElement : Element +{ + public NoneElement() + { + Name = "NoneElement"; + SpdxId = "SPDXRef-None"; + Type = nameof(Element); + } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Organization.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Organization.cs new file mode 100644 index 00000000..3fe26360 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Organization.cs @@ -0,0 +1,13 @@ +// 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.Parsers.Spdx30SbomParser.Entities; + +using System.Text.Json.Serialization; + +/// +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Organization/ +/// +public class Organization : Element +{ +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Package.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Package.cs new file mode 100644 index 00000000..a0a95f94 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Package.cs @@ -0,0 +1,26 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// Represents a SPDX 3.0 Package. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/Package/ +/// +public class Package : Software +{ + public Package() + { + Type = "software_Package"; + } + + /// + /// Gets or sets the name and optional contact information of the person or organization that built this package. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("suppliedBy")] + public string SuppliedBy { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/PackageVerificationCode.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/PackageVerificationCode.cs new file mode 100644 index 00000000..f5f52068 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/PackageVerificationCode.cs @@ -0,0 +1,33 @@ +// 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.Text.Json.Serialization; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// Represents the hash value of the element using the algorithm specified. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/PackageVerificationCode/ +/// +public class PackageVerificationCode : Element +{ + /// + /// Gets or sets the algorithm being used to calculate the type of verification. + /// + [JsonRequired] + [JsonPropertyName("algorithm")] + public HashAlgorithm Algorithm { get; set; } + + /// + /// Gets or sets the string value of the algorithm being used to calculate the type of verification. + /// + [JsonRequired] + [JsonPropertyName("hashValue")] + public string HashValue { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("packageVerificationCodeExcludedFile")] + public List PackageVerificationCodeExcludedFile { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Person.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Person.cs new file mode 100644 index 00000000..5aae0be1 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Person.cs @@ -0,0 +1,13 @@ +// 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.Parsers.Spdx30SbomParser.Entities; + +using System.Text.Json.Serialization; + +/// +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Person/ +/// +public class Person : Element +{ +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Snippet.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Snippet.cs new file mode 100644 index 00000000..4e2a96e2 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Snippet.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.Parsers.Spdx30SbomParser.Entities; + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +/// +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/Snippet/ +/// +public class Snippet : Software +{ +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Software.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Software.cs new file mode 100644 index 00000000..a2e2ed0a --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Software.cs @@ -0,0 +1,82 @@ +// 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.Text.Json.Serialization; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; +using Newtonsoft.Json; +using JsonIgnoreAttribute = System.Text.Json.Serialization.JsonIgnoreAttribute; + +/// +/// Class defined as specified in: https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Software/ +/// +public abstract class Software : Element +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_additionalPurpose")] + public object AdditionalPurpose { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_attributionText")] + public string AttributionText { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("software_byteRange")] + public int ByteRange { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_contentIdentifier")] + public ContentIdentifier ContentIdentifier { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_contentIdentifierType")] + public virtual string ContentIdentifierType { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_contentIdentifierValue")] + public string ContentIdentifierValue { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonProperty(PropertyName = "software_copyrightText")] + public string CopyrightText { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_downloadLocation")] + public string DownloadLocation { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_fileKind")] + public object FileKind { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_homePage")] + public string HomePage { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("software_lineRange")] + public int LineRange { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_packageUrl")] + public string PackageUrl { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_packageVersion")] + public string PackageVersion { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_primaryPurpose")] + public object PrimaryPurpose { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_sbomType")] + public object SbomType { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_snippetFromFile")] + public File SnippetFromFile { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_copyrightText")] + public string SourceInfo { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Spdx30Relationship.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Spdx30Relationship.cs new file mode 100644 index 00000000..974e8940 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Spdx30Relationship.cs @@ -0,0 +1,55 @@ +// 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.Text.Json.Serialization; +using Microsoft.Sbom.Extensions.Entities; +using RelationshipType = Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums.RelationshipType; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// Defines relationships between elements in the current SBOM. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Relationship/ +/// +public class Spdx30Relationship : Element +{ + /// + /// Initializes a new instance of the class. + /// + public Spdx30Relationship() + { + Type = nameof(Relationship); + } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completeness")] + public object Completeness { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("endTime")] + public DateTime EndTime { get; set; } + + /// + /// Gets or sets the id of the source element with whom the target element has a relationship. + /// + [JsonRequired] + [JsonPropertyName("from")] + public string From { get; set; } + + [JsonRequired] + [JsonPropertyName("relationshipType")] + public RelationshipType RelationshipType { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("startTime")] + public DateTime StartTime { get; set; } + + /// + /// Gets or sets the id of the target element with whom the source element has a relationship. + /// + [JsonRequired] + [JsonPropertyName("to")] + public List To { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/SpdxDocument.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/SpdxDocument.cs new file mode 100644 index 00000000..04284f74 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/SpdxDocument.cs @@ -0,0 +1,39 @@ +// 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.Text.Json.Serialization; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +/// +/// The SpdxDocument provides a convenient way to express information about collections of SPDX Elements that could potentially be serialized as complete units (e.g., all in-scope SPDX data within a single JSON-LD file). +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/SpdxDocument/ +/// +public class SpdxDocument : Element +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("dataLicense")] + public string DataLicense { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("import")] + public List Import { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("namespaceMap")] + public List NamespaceMap { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("element")] + public List Element { get; set; } + + [JsonRequired] + [JsonPropertyName("profileConformance")] + public List ProfileConformance { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("rootElement")] + public List RootElement { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Tool.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Tool.cs new file mode 100644 index 00000000..082ca6f8 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Tool.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.Parsers.Spdx30SbomParser.Entities; + +using System.Text.Json.Serialization; + +/// +/// A tool is an element of hardware and/or software utilized to carry out a particular function. +/// https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Tool/ +/// +public class Tool : Element +{ +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Exceptions/MissingHashValueException.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Exceptions/MissingHashValueException.cs new file mode 100644 index 00000000..7b178756 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Exceptions/MissingHashValueException.cs @@ -0,0 +1,24 @@ +// 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.Runtime.Serialization; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Exceptions; + +internal class MissingHashValueException : Exception +{ + public MissingHashValueException() + { + } + + public MissingHashValueException(string message) + : base(message) + { + } + + public MissingHashValueException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Generator.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Generator.cs new file mode 100644 index 00000000..4b61b0cb --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Generator.cs @@ -0,0 +1,629 @@ +// 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.ComponentModel; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks.Sources; +using System.Xml.Linq; +using Microsoft.Sbom.Common; +using Microsoft.Sbom.Contracts; +using Microsoft.Sbom.Contracts.Enums; +using Microsoft.Sbom.Extensions; +using Microsoft.Sbom.Extensions.Entities; +using Microsoft.Sbom.Parsers.Spdx30SbomParser; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Exceptions; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Utils; +using RelationshipType = Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums.RelationshipType; +using SHA1 = System.Security.Cryptography.SHA1; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser; + +/// +/// Generates SPDX 3.0 format elements which are used to make up the SBOM document. +/// +public class Generator : IManifestGenerator +{ + private static readonly Dictionary AlgorithmMap = new() + { + { AlgorithmName.SHA1, HashAlgorithm.sha1 }, + { AlgorithmName.SHA256, HashAlgorithm.sha256 }, + { AlgorithmName.SHA512, HashAlgorithm.sha512 }, + { AlgorithmName.MD5, HashAlgorithm.md5 } + }; + + public AlgorithmName[] RequiredHashAlgorithms => new[] { AlgorithmName.SHA256, AlgorithmName.SHA1 }; + + public string Version { get; set; } = string.Join("-", Constants.SPDXName, Constants.SPDXVersion); + + string IManifestGenerator.FilesArrayHeaderName => throw new NotSupportedException(); + + string IManifestGenerator.PackagesArrayHeaderName => throw new NotSupportedException(); + + string IManifestGenerator.RelationshipsArrayHeaderName => throw new NotSupportedException(); + + string IManifestGenerator.ExternalDocumentRefArrayHeaderName => throw new NotSupportedException(); + + private JsonSerializerOptions serializerOptions = new JsonSerializerOptions + { + Converters = { new ElementSerializer() }, + }; + + /// + /// Generates all SPDX elements related to a single file. + /// + /// SBOM file info that needs to be translated to SPDX elements. + /// + /// + public GenerationResult GenerateJsonDocument(InternalSbomFileInfo fileInfo) + { + if (fileInfo is null) + { + throw new ArgumentNullException(nameof(fileInfo)); + } + + var spdxFileAndRelationshipElements = ConvertSbomFileToSpdxFileAndRelationships(fileInfo); + + return new GenerationResult + { + Document = JsonDocument.Parse(JsonSerializer.Serialize(spdxFileAndRelationshipElements, this.serializerOptions)), + ResultMetadata = new ResultMetadata + { + EntityId = spdxFileAndRelationshipElements.First().SpdxId, + }, + }; + } + + /// + /// Generate all SPDX elements related to a package. + /// + /// Package info to be translated to SPDX elements. + /// + /// + public GenerationResult GenerateJsonDocument(SbomPackage packageInfo) + { + if (packageInfo is null) + { + throw new ArgumentNullException(nameof(packageInfo)); + } + + if (packageInfo.PackageName is null) + { + throw new ArgumentNullException(nameof(packageInfo.PackageName)); + } + + var spdxSupplier = new Organization + { + Name = packageInfo.Supplier ?? Constants.NoAssertionValue, + }; + spdxSupplier.AddSpdxId(); + + var spdxPackage = new Package + { + Name = packageInfo.PackageName, + PackageVersion = packageInfo.PackageVersion, + DownloadLocation = packageInfo.PackageSource ?? Constants.NoAssertionValue, + CopyrightText = packageInfo.CopyrightText ?? Constants.NoAssertionValue, + SuppliedBy = spdxSupplier.SpdxId, + }; + var packageId = SPDXExtensions.GetSpdxElementId(packageInfo); + spdxPackage.AddSpdxId(packageId); + + var spdxRelationshipAndLicensesFromSbomPackage = GetSpdxRelationshipsAndLicensesFromSbomPackage(packageInfo, spdxPackage); + + // Add external identifier based on package url and link it back to the package it's related to by setting spdxPackage.ExternalIdentifier + ExternalIdentifier spdxExternalIdentifier = null; + if (packageInfo.PackageUrl != null) + { + spdxExternalIdentifier = new ExternalIdentifier + { + ExternalIdentifierType = "purl", + Identifier = packageInfo.PackageUrl + }; + } + + spdxExternalIdentifier.AddSpdxId(); + spdxPackage.ExternalIdentifier = new List { spdxExternalIdentifier.SpdxId }; + + var spdxElementsRelatedToPackageInfo = new List + { + spdxSupplier, + spdxPackage, + spdxExternalIdentifier, + }; + spdxElementsRelatedToPackageInfo.AddRange(spdxRelationshipAndLicensesFromSbomPackage); + + var dependOnId = packageInfo.DependOn; + if (dependOnId is not null && dependOnId != Constants.RootPackageIdValue) + { + dependOnId = SPDXExtensions.GenerateSpdxId(spdxPackage, packageInfo.DependOn); + } + + return new GenerationResult + { + Document = JsonDocument.Parse(JsonSerializer.Serialize(spdxElementsRelatedToPackageInfo, this.serializerOptions)), + ResultMetadata = new ResultMetadata + { + EntityId = spdxPackage.SpdxId, + DependOn = dependOnId + } + }; + } + + /// + /// Generate root package SPDX elements. + /// + /// Metadata that includes info about root package. + /// + /// + public GenerationResult GenerateRootPackage(IInternalMetadataProvider internalMetadataProvider) + { + if (internalMetadataProvider is null) + { + throw new ArgumentNullException(nameof(internalMetadataProvider)); + } + + var spdxExternalIdentifier = new ExternalIdentifier + { + ExternalIdentifierType = "purl", + Identifier = internalMetadataProvider.GetSwidTagId(), + }; + spdxExternalIdentifier.AddSpdxId(); + + var spdxSupplier = new Organization + { + Name = string.Format(Constants.PackageSupplierFormatString, internalMetadataProvider.GetPackageSupplier()), + }; + + // Bare minimum package details. + var spdxPackage = new Package + { + SpdxId = Constants.RootPackageIdValue, + Name = internalMetadataProvider.GetPackageName(), + PackageVersion = internalMetadataProvider.GetPackageVersion(), + ExternalIdentifier = new List { spdxExternalIdentifier.SpdxId }, + DownloadLocation = Constants.NoAssertionValue, + CopyrightText = Constants.NoAssertionValue, + VerifiedUsing = new List { GetPackageVerificationCode(internalMetadataProvider) }, + SuppliedBy = spdxSupplier.SpdxId, + }; + + // Generate SPDX relationship elements to indicate no assertions are made about licenses for this root package. + var noAssertionLicense = GenerateLicenseElement(null); + + var spdxRelationshipLicenseDeclaredElement = new Spdx30Relationship + { + From = spdxPackage.SpdxId, + RelationshipType = RelationshipType.HAS_DECLARED_LICENSE, + To = new List { noAssertionLicense.SpdxId }, + }; + + var spdxRelationshipLicenseConcludedElement = new Spdx30Relationship + { + From = spdxPackage.SpdxId, + RelationshipType = RelationshipType.HAS_CONCLUDED_LICENSE, + To = new List { noAssertionLicense.SpdxId }, + }; + + spdxSupplier.AddSpdxId(); + spdxPackage.AddSpdxId(); + spdxRelationshipLicenseDeclaredElement.AddSpdxId(); + spdxRelationshipLicenseConcludedElement.AddSpdxId(); + + var spdxElementsRelatedToRootPackage = new List + { + spdxExternalIdentifier, + spdxSupplier, + spdxPackage, + noAssertionLicense, + spdxRelationshipLicenseDeclaredElement, + spdxRelationshipLicenseConcludedElement, + }; + + return new GenerationResult + { + Document = JsonDocument.Parse(JsonSerializer.Serialize(spdxElementsRelatedToRootPackage, this.serializerOptions)), + ResultMetadata = new ResultMetadata + { + EntityId = Constants.RootPackageIdValue, + DocumentId = Constants.SPDXDocumentIdValue + } + }; + } + + /// + /// Convert external document reference info to SPDX elements. + /// + /// External document reference info. + /// + /// + /// + public GenerationResult GenerateJsonDocument(ExternalDocumentReferenceInfo externalDocumentReferenceInfo) + { + if (externalDocumentReferenceInfo is null) + { + throw new ArgumentNullException(nameof(externalDocumentReferenceInfo)); + } + + if (externalDocumentReferenceInfo.Checksum is null) + { + throw new ArgumentNullException(nameof(externalDocumentReferenceInfo.Checksum)); + } + + var sha1Hash = externalDocumentReferenceInfo.Checksum.FirstOrDefault(h => h.Algorithm == AlgorithmName.SHA1) ?? + throw new MissingHashValueException( + $"The hash value for algorithm {AlgorithmName.SHA1} is missing from {nameof(externalDocumentReferenceInfo)}"); + var checksumValue = sha1Hash.ChecksumValue.ToLower(); + + var packageVerificationCode = new PackageVerificationCode + { + Algorithm = HashAlgorithm.sha1, + HashValue = checksumValue + }; + packageVerificationCode.AddSpdxId(); + + var spdxExternalMap = new ExternalMap + { + VerifiedUsing = new List + { + packageVerificationCode + }, + ExternalSpdxId = externalDocumentReferenceInfo.DocumentNamespace, + }; + + spdxExternalMap.AddExternalSpdxId(externalDocumentReferenceInfo.ExternalDocumentName, externalDocumentReferenceInfo.Checksum); + spdxExternalMap.AddSpdxId(); + var externalDocumentReferenceId = spdxExternalMap.ExternalSpdxId; + + return new GenerationResult + { + Document = JsonDocument.Parse(JsonSerializer.Serialize(spdxExternalMap, this.serializerOptions)), + ResultMetadata = new ResultMetadata + { + EntityId = externalDocumentReferenceId + } + }; + } + + /// + /// Generate SPDX elements related to a relationship. + /// + /// Relationship info + /// + /// + public GenerationResult GenerateJsonDocument(Relationship relationship) + { + if (relationship is null) + { + throw new ArgumentNullException(nameof(relationship)); + } + + // If target spdxFileElement in spdxRelationship has external reference ID, we will concatenate it together according to SPDX 2.2 standard. + // In 3.0 this concatenation is not required, however we will retain this behavior for compatibility with SPDX 2.2. + var targetElement = !string.IsNullOrEmpty(relationship.TargetElementExternalReferenceId) ? + $"{relationship.TargetElementExternalReferenceId}:{relationship.TargetElementId}" + : relationship.TargetElementId; + var sourceElement = relationship.SourceElementId; + + var spdxRelationship = new Spdx30Relationship + { + From = sourceElement, + RelationshipType = this.GetSPDXRelationshipType(relationship.RelationshipType), + To = new List { targetElement }, + }; + spdxRelationship.AddSpdxId(); + + return new GenerationResult + { + Document = JsonDocument.Parse(JsonSerializer.Serialize(spdxRelationship, this.serializerOptions)), + }; + } + + /// + /// Generate all SPDX elements related to document creation. + /// + /// Document metadata + /// + /// + public GenerationResult GenerateJsonDocument(IInternalMetadataProvider internalMetadataProvider) + { + if (internalMetadataProvider is null) + { + throw new ArgumentNullException(nameof(internalMetadataProvider)); + } + + var generationData = internalMetadataProvider.GetGenerationData(Constants.Spdx30ManifestInfo); + + var sbomToolName = internalMetadataProvider.GetMetadata(MetadataKey.SBOMToolName); + var sbomToolVersion = internalMetadataProvider.GetMetadata(MetadataKey.SBOMToolVersion); + var packageName = internalMetadataProvider.GetPackageName(); + var packageVersion = internalMetadataProvider.GetPackageVersion(); + + var orgName = internalMetadataProvider.GetPackageSupplier(); + var toolName = sbomToolName + "-" + sbomToolVersion; + var documentName = string.Format(Constants.SPDXDocumentNameFormatString, packageName, packageVersion); + + var spdxOrganization = new Organization + { + Name = orgName, + }; + + var spdxTool = new Tool + { + Name = toolName, + }; + + spdxOrganization.AddSpdxId(); + spdxTool.AddSpdxId(); + + var spdxCreationInfo = new CreationInfo + { + Id = "_:creationinfo", + SpecVersion = Constants.SPDXVersion, + Created = internalMetadataProvider.GetGenerationTimestamp(), + CreatedBy = new List { spdxOrganization.SpdxId }, + CreatedUsing = new List { spdxTool.SpdxId }, + }; + + var spdxNamespaceMap = new NamespaceMap + { + Namespace = internalMetadataProvider.GetDocumentNamespace(), + }; + + spdxNamespaceMap.AddSpdxId(); + + var spdxDataLicense = new AnyLicenseInfo + { + Name = Constants.DataLicenceValue, + }; + spdxDataLicense.AddSpdxId(); + + var spdxDocument = new SpdxDocument + { + DataLicense = spdxDataLicense.SpdxId, + NamespaceMap = new List { spdxNamespaceMap }, + SpdxId = Constants.SPDXDocumentIdValue, + Name = documentName, + ProfileConformance = new List { ProfileIdentifierType.software, ProfileIdentifierType.core, ProfileIdentifierType.simpleLicensing }, + }; + + spdxDocument.AddSpdxId(); + + var spdxRelationship = new Spdx30Relationship + { + From = spdxDataLicense.SpdxId, + RelationshipType = RelationshipType.HAS_DECLARED_LICENSE, + To = new List { spdxDocument.SpdxId }, + }; + + spdxCreationInfo.AddSpdxId(); + spdxRelationship.AddSpdxId(); + + var spdxElementsRelatedToDocCreation = new List { spdxOrganization, spdxTool, spdxCreationInfo, spdxDataLicense, spdxDocument, spdxRelationship }; + return new GenerationResult + { + Document = JsonDocument.Parse(JsonSerializer.Serialize(spdxElementsRelatedToDocCreation, this.serializerOptions)), + ResultMetadata = new ResultMetadata + { + EntityId = spdxDocument.SpdxId, + }, + }; + } + + /// + /// Use file info to generate file and relationship spdx elements. + /// + /// SBOM file info that needs to be translated to SPDX elements. + /// + /// + /// + private List ConvertSbomFileToSpdxFileAndRelationships(InternalSbomFileInfo fileInfo) + { + if (fileInfo is null) + { + throw new ArgumentNullException(nameof(fileInfo)); + } + + if (fileInfo.Checksum?.Any() == false) + { + throw new ArgumentException(nameof(fileInfo.Checksum)); + } + + if (string.IsNullOrWhiteSpace(fileInfo.Path)) + { + throw new ArgumentException(nameof(fileInfo.Path)); + } + + GeneratorUtils.EnsureRequiredHashesPresent(fileInfo.Checksum.ToArray(), RequiredHashAlgorithms); + + var packageVerificationCodes = new List(); + foreach (var checksum in fileInfo.Checksum) + { + var packageVerificationCode = new PackageVerificationCode + { + Algorithm = AlgorithmMap.GetValueOrDefault(checksum.Algorithm), + HashValue = checksum.ChecksumValue.ToLowerInvariant(), + }; + packageVerificationCode.AddSpdxId(); + packageVerificationCodes.Add(packageVerificationCode); + } + + // Generate SPDX file element + var spdxFileElement = new Entities.File + { + VerifiedUsing = packageVerificationCodes, + Name = GeneratorUtils.EnsureRelativePathStartsWithDot(fileInfo.Path), + CopyrightText = fileInfo.FileCopyrightText ?? Constants.NoAssertionValue, + }; + var fileId = SPDXExtensions.GetSpdxFileId(fileInfo.Path, fileInfo.Checksum); + spdxFileElement.AddSpdxId(fileId); + + // Generate SPDX spdxRelationship elements + var spdxRelationshipsFromSbomFile = GetSpdxRelationshipsFromSbomFile(spdxFileElement, fileInfo); + + // Return all spdx elements related to the file info + var spdxElementsRelatedToFileInfo = new List { spdxFileElement }; + spdxElementsRelatedToFileInfo.AddRange(spdxRelationshipsFromSbomFile); + + return spdxElementsRelatedToFileInfo; + } + + private List GetSpdxRelationshipsFromSbomFile(Element spdxFileElement, InternalSbomFileInfo fileInfo) + { + var spdxRelationshipAndLicenseElementsToAddToSBOM = new List(); + + // Convert licenseConcluded to SPDX license element and add a Relationship element for it + var licenseConcludedElement = GenerateLicenseElement(fileInfo.LicenseConcluded); + spdxRelationshipAndLicenseElementsToAddToSBOM.Add(licenseConcludedElement); + + var spdxRelationshipLicenseConcludedElement = new Spdx30Relationship + { + From = spdxFileElement.SpdxId, + RelationshipType = RelationshipType.HAS_CONCLUDED_LICENSE, + To = new List { licenseConcludedElement.SpdxId } + }; + + spdxRelationshipLicenseConcludedElement.AddSpdxId(); + spdxRelationshipAndLicenseElementsToAddToSBOM.Add(spdxRelationshipLicenseConcludedElement); + + // Convert licenseDeclared to SPDX license elements and add Relationship elements for them + var toRelationships = new List(); + foreach (var licenseInfoInOneFile in fileInfo.LicenseInfoInFiles) + { + var licenseDeclaredElement = GenerateLicenseElement(licenseInfoInOneFile); + toRelationships.Add(licenseDeclaredElement.SpdxId); + } + + var spdxRelationshipLicenseDeclaredElement = new Spdx30Relationship + { + From = spdxFileElement.SpdxId, + RelationshipType = RelationshipType.HAS_DECLARED_LICENSE, + To = toRelationships, + }; + + spdxRelationshipLicenseDeclaredElement.AddSpdxId(); + spdxRelationshipAndLicenseElementsToAddToSBOM.Add(spdxRelationshipLicenseDeclaredElement); + + return spdxRelationshipAndLicenseElementsToAddToSBOM; + } + + private List GetSpdxRelationshipsAndLicensesFromSbomPackage(SbomPackage packageInfo, Element spdxPackage) + { + var spdxRelationshipAndLicenseElementsToAddToSBOM = new List(); + + // Convert licenseConcluded to SPDX spdxFileElement and add a Relationship spdxFileElement for it + var licenseConcludedElement = GenerateLicenseElement(packageInfo.LicenseInfo?.Concluded); + spdxRelationshipAndLicenseElementsToAddToSBOM.Add(licenseConcludedElement); + + var spdxRelationshipLicenseConcludedElement = new Spdx30Relationship + { + From = spdxPackage.SpdxId, + RelationshipType = RelationshipType.HAS_CONCLUDED_LICENSE, + To = new List { licenseConcludedElement.SpdxId } + }; + + spdxRelationshipLicenseConcludedElement.AddSpdxId(); + spdxRelationshipAndLicenseElementsToAddToSBOM.Add(spdxRelationshipLicenseConcludedElement); + + // Convert licenseDeclared to SPDX elements and add a Relationship spdxFileElement for them + var licenseDeclaredElement = GenerateLicenseElement(packageInfo.LicenseInfo?.Declared); + spdxRelationshipAndLicenseElementsToAddToSBOM.Add(licenseDeclaredElement); + + var spdxRelationshipLicenseDeclaredElement = new Spdx30Relationship + { + From = spdxPackage.SpdxId, + RelationshipType = RelationshipType.HAS_DECLARED_LICENSE, + To = new List { licenseDeclaredElement.SpdxId } + }; + + spdxRelationshipLicenseDeclaredElement.AddSpdxId(); + spdxRelationshipAndLicenseElementsToAddToSBOM.Add(spdxRelationshipLicenseDeclaredElement); + + return spdxRelationshipAndLicenseElementsToAddToSBOM; + } + + private Element GenerateLicenseElement(string licenseInfo) + { + Element licenseElement = null; + if (licenseInfo == null) + { + licenseElement = new NoAssertionElement(); + } + else + { + licenseElement = new AnyLicenseInfo { Name = licenseInfo }; + } + + licenseElement.AddSpdxId(); + return licenseElement; + } + + /// + /// Convert Extensions.Entities.RelationshipType to SPDX 3.0 RelationshipType. + /// + /// + /// + /// + private RelationshipType GetSPDXRelationshipType(Extensions.Entities.RelationshipType relationshipType) + { + switch (relationshipType) + { + case Extensions.Entities.RelationshipType.CONTAINS: return RelationshipType.CONTAINS; + case Extensions.Entities.RelationshipType.DEPENDS_ON: return RelationshipType.DEPENDS_ON; + case Extensions.Entities.RelationshipType.DESCRIBES: return RelationshipType.DESCRIBES; + case Extensions.Entities.RelationshipType.PREREQUISITE_FOR: return RelationshipType.HAS_PREREQUISITE; + case Extensions.Entities.RelationshipType.DESCRIBED_BY: return RelationshipType.DESCRIBES; + case Extensions.Entities.RelationshipType.PATCH_FOR: return RelationshipType.PATCHED_BY; + default: + throw new NotImplementedException($"The spdxRelationship {relationshipType} is currently not " + + $"mapped to any SPDX 3.0 spdxRelationship type."); + } + } + + /// + /// Generates the package verification code for a given package using the SPDX 3.0 specification. + /// + /// Algorithm defined here https://spdx.github.io/spdx-spec/v2.2.2/package-information/#79-package-verification-code-field. + /// + /// + /// + private PackageVerificationCode GetPackageVerificationCode(IInternalMetadataProvider internalMetadataProvider) + { + // Get a list of SHA1 checksums + IList sha1Checksums = new List(); + foreach (var checksumArray in internalMetadataProvider.GetGenerationData(Constants.Spdx30ManifestInfo).Checksums) + { + sha1Checksums.Add(checksumArray + .Where(c => c.Algorithm == AlgorithmName.SHA1) + .Select(c => c.ChecksumValue) + .FirstOrDefault()); + } + + var packageChecksumString = string.Join(string.Empty, sha1Checksums.OrderBy(s => s)); +#pragma warning disable CA5350 // Suppress Do Not Use Weak Cryptographic Algorithms as we use SHA1 intentionally + var sha1Hasher = SHA1.Create(); +#pragma warning restore CA5350 + var hashByteArray = sha1Hasher.ComputeHash(Encoding.Default.GetBytes(packageChecksumString)); + + var packageVerificationCode = new PackageVerificationCode + { + Algorithm = HashAlgorithm.sha1, + HashValue = Convert.ToHexString(hashByteArray).Replace("-", string.Empty).ToLowerInvariant(), + }; + packageVerificationCode.AddSpdxId(); + return packageVerificationCode; + } + + public ManifestInfo RegisterManifest() => Constants.Spdx30ManifestInfo; + + IDictionary IManifestGenerator.GetMetadataDictionary(IInternalMetadataProvider internalMetadataProvider) => throw new NotSupportedException(); +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Microsoft.Sbom.Parsers.Spdx30SbomParser.csproj b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Microsoft.Sbom.Parsers.Spdx30SbomParser.csproj new file mode 100644 index 00000000..95b22625 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Microsoft.Sbom.Parsers.Spdx30SbomParser.csproj @@ -0,0 +1,26 @@ + + + + Microsoft.Sbom.Parsers.Spdx30SbomParser + True + SPDX3.0 parser for SBOM tool. + True + $(StrongNameSigningKeyFilePath) + + + + + + + + + + + <_Parameter1>$(AssemblyName).Tests, PublicKey=$(StrongNameSigningPublicKey) + + + + + + + diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/ElementSerializer.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/ElementSerializer.cs new file mode 100644 index 00000000..c47af346 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/ElementSerializer.cs @@ -0,0 +1,29 @@ +// 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.Text.Json; +using System.Text.Json.Serialization; +using System.Xml.Linq; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +public class ElementSerializer : JsonConverter> +{ + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException("Deserialization of Elements into specific subtypes is not implemented yet."); + } + + public override void Write(Utf8JsonWriter writer, List elements, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (var element in elements) + { + JsonSerializer.Serialize(writer, element, element.GetType(), options); + } + + writer.WriteEndArray(); + } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/SPDXExtensions.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/SPDXExtensions.cs new file mode 100644 index 00000000..0b8cfbfc --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/SPDXExtensions.cs @@ -0,0 +1,173 @@ +// 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.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Sbom.Contracts; +using Microsoft.Sbom.Contracts.Entities; +using Microsoft.Sbom.Contracts.Enums; +using Microsoft.Sbom.Extensions.Entities; +using Microsoft.Sbom.Extensions.Exceptions; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.Enums; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser.Utils; + +/// +/// Provides extensions to SPDX objects. +/// +public static class SPDXExtensions +{ + /// + /// Only these chars are allowed in a SPDX id. Replace all other chars with '-'. + /// + private static readonly Regex SpdxIdAllowedCharsRegex = new Regex("[^a-zA-Z0-9.-]"); + + /// + /// Returns the SPDX-compliant ID for a general element. + /// The element to generate the ID for. + /// The ID that uniquely identifies an element to generate the hash for. + /// + public static string GenerateSpdxId(Element element, string id) => $"SPDXRef-{element.Type}-{GetStringHash(id)}"; + + /// + /// Returns the SPDX-compliant external document ID. + /// + public static string GenerateSpdxExternalDocumentId(string fileName, string sha1Value) + { + var spdxExternalDocumentId = $"DocumentRef-{fileName}-{sha1Value}"; + return SpdxIdAllowedCharsRegex.Replace(spdxExternalDocumentId, "-"); + } + + /// + /// Get's an ID that corresponds to the package info + /// + /// + /// + /// Package ID that encapsulates unique info about a package. + public static string GetSpdxElementId(SbomPackage packageInfo) + { + if (packageInfo is null) + { + throw new ArgumentNullException(nameof(packageInfo)); + } + + // Get package identity as package name and package version. If version is empty, just use package name + var packageIdentity = $"{packageInfo.Type}-{packageInfo.PackageName}"; + if (!string.IsNullOrWhiteSpace(packageInfo.PackageVersion)) + { + packageIdentity = string.Join("-", packageInfo.Type, packageInfo.PackageName, packageInfo.PackageVersion); + } + + return packageInfo.Id ?? packageIdentity; + } + + /// + /// Adds a SPDXID property to the given file. The id of the file should be the same + /// for any build as long as the contents of the file haven't changed. + /// + /// + /// + public static string GetSpdxFileId(string fileName, IEnumerable checksums) + { + if (string.IsNullOrEmpty(fileName)) + { + throw new ArgumentException($"'{nameof(fileName)}' cannot be null or empty.", nameof(fileName)); + } + + if (checksums is null || !checksums.Any(c => c.Algorithm == AlgorithmName.SHA1)) + { + throw new MissingHashValueException($"The file {fileName} is missing the {HashAlgorithmName.SHA1} hash value."); + } + + // Get the SHA1 for this file. + var sha1Value = checksums.Where(c => c.Algorithm == AlgorithmName.SHA1) + .Select(s => s.ChecksumValue) + .FirstOrDefault(); + + return sha1Value; + } + + /// + /// Adds ExternalSpdxId property to the SPDXExternalDocumentReference based on name and checksum information. + /// + public static void AddExternalSpdxId(this ExternalMap reference, string name, IEnumerable checksums) + { + if (reference is null) + { + throw new ArgumentNullException(nameof(reference)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); + } + + var sha1checksums = checksums.Where(c => c.Algorithm == AlgorithmName.SHA1); + if (checksums is null || !sha1checksums.Any()) + { + throw new MissingHashValueException($"The external reference {name} is missing the {HashAlgorithmName.SHA1} hash value."); + } + + // Get the SHA1 for this file. + var sha1Value = sha1checksums.FirstOrDefault().ChecksumValue; + + reference.ExternalSpdxId = GenerateSpdxExternalDocumentId(name, sha1Value); + } + + public static void AddSpdxId(this Element element) + { + element.SpdxId = GenerateSpdxId(element, element.Name); + } + + public static void AddSpdxId(this NamespaceMap namespaceMap) + { + namespaceMap.SpdxId = GenerateSpdxId(namespaceMap, namespaceMap.Namespace); + } + + public static void AddSpdxId(this CreationInfo creationInfo) + { + creationInfo.SpdxId = GenerateSpdxId(creationInfo, creationInfo.Id); + } + + public static void AddSpdxId(this Spdx30Relationship relationship) + { + relationship.SpdxId = GenerateSpdxId(relationship, relationship.To + relationship.RelationshipType.ToString()); + } + + public static void AddSpdxId(this ExternalIdentifier externalIdentifier) + { + externalIdentifier.SpdxId = GenerateSpdxId(externalIdentifier, externalIdentifier.Identifier.ToString()); + } + + public static void AddSpdxId(this ExternalMap externalMap) + { + externalMap.SpdxId = GenerateSpdxId(externalMap, externalMap.ExternalSpdxId); + } + + public static void AddSpdxId(this PackageVerificationCode packageVerificationCode) + { + packageVerificationCode.SpdxId = GenerateSpdxId(packageVerificationCode, packageVerificationCode.Algorithm.ToString()); + } + + public static void AddSpdxId(this Element element, string id) + { + element.SpdxId = GenerateSpdxId(element, id); + } + + /// Compute the SHA256 string representation (omitting dashes) of a given string + /// + /// + /// TODO: refactor this into Core as similar functionality is duplicated in a few different places in the codebase + /// + private static string GetStringHash(string str) + { + var hash = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(str)); + var spdxId = Convert.ToHexString(hash).Replace("-", string.Empty); + return spdxId; + } +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx22SbomParser.Tests/Utils/InternalMetadataProviderIdentityExtensionsTests.cs b/test/Microsoft.Sbom.Parsers.Spdx22SbomParser.Tests/Utils/InternalMetadataProviderIdentityExtensionsTests.cs index 6acf2e19..22195d59 100644 --- a/test/Microsoft.Sbom.Parsers.Spdx22SbomParser.Tests/Utils/InternalMetadataProviderIdentityExtensionsTests.cs +++ b/test/Microsoft.Sbom.Parsers.Spdx22SbomParser.Tests/Utils/InternalMetadataProviderIdentityExtensionsTests.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Text.RegularExpressions; +using Microsoft.Sbom.Common; using Microsoft.Sbom.Extensions; using Microsoft.Sbom.Extensions.Entities; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests.csproj b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests.csproj new file mode 100644 index 00000000..4b348b38 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests.csproj @@ -0,0 +1,17 @@ + + + + false + $(NoWarn);NU1605 + Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests + True + $(StrongNameSigningKeyFilePath) + + + + + + + + + diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/GeneratorTests.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/GeneratorTests.cs new file mode 100644 index 00000000..7f0a2222 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/GeneratorTests.cs @@ -0,0 +1,237 @@ +// 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.Text.RegularExpressions; +using Microsoft.Sbom.Api.Manifest.Configuration; +using Microsoft.Sbom.Api.Metadata; +using Microsoft.Sbom.Api.Output.Telemetry; +using Microsoft.Sbom.Api.Recorder; +using Microsoft.Sbom.Common; +using Microsoft.Sbom.Common.Config; +using Microsoft.Sbom.Contracts; +using Microsoft.Sbom.Contracts.Enums; +using Microsoft.Sbom.Extensions; +using Microsoft.Sbom.Extensions.Entities; +using Microsoft.Sbom.Parser.JsonStrings; +using Microsoft.Sbom.Parsers.Spdx30SbomParser; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Constants = Microsoft.Sbom.Parsers.Spdx30SbomParser.Constants; +using ILogger = Serilog.ILogger; + +namespace Microsoft.Sbom.Parser; + +[TestClass] +public class GeneratorTests +{ + private readonly Generator generator = new Generator(); + private readonly Mock recorderMock = new Mock(MockBehavior.Strict); + private readonly Mock mockLogger = new Mock(MockBehavior.Strict); + private readonly Mock fileSystemMock = new Mock(MockBehavior.Strict); + private readonly Mock mockConfigHandler = new Mock(MockBehavior.Strict); + + [TestMethod] + public void GenerateJsonDocumentTest_DocumentCreation() + { + var sbomConfigs = CreateInternalMetadataProvider(); + var generatorResult = generator.GenerateJsonDocument(sbomConfigs); + var generatedJsonString = generatorResult.Document.RootElement.GetRawText(); + generatedJsonString = NormalizeString(generatedJsonString); + + var expectedJsonContentAsString = SbomDocCreationJsonStrings.DocCreationJsonString; + expectedJsonContentAsString = NormalizeString(expectedJsonContentAsString); + var regexPattern = ConvertJsonToRegex(expectedJsonContentAsString); + + Assert.IsFalse(generatedJsonString.Contains("null")); + Assert.IsTrue(Regex.IsMatch(generatedJsonString, regexPattern)); + } + + [TestMethod] + public void GenerateJsonDocumentTest_RootPackage() + { + var sbomConfigs = CreateInternalMetadataProvider(); + + var generatorResult = generator.GenerateRootPackage(sbomConfigs); + var generatedJsonString = generatorResult.Document.RootElement.GetRawText(); + generatedJsonString = NormalizeString(generatedJsonString); + + var expectedJsonContentAsString = SbomPackageJsonStrings.RootPackageJsonString; + expectedJsonContentAsString = NormalizeString(expectedJsonContentAsString); + var regexPattern = ConvertJsonToRegex(expectedJsonContentAsString); + + Assert.IsFalse(generatedJsonString.Contains("null")); + Assert.IsTrue(Regex.IsMatch(generatedJsonString, regexPattern)); + } + + [TestMethod] + public void GenerateJsonDocumentTest_Package() + { + var packageInfo = new SbomPackage + { + PackageName = "test", + PackageUrl = "packageUrl", + FilesAnalyzed = false + }; + + var generatorResult = generator.GenerateJsonDocument(packageInfo); + var generatedJsonString = generatorResult.Document.RootElement.GetRawText(); + generatedJsonString = NormalizeString(generatedJsonString); + + var expectedJsonContentAsString = SbomPackageJsonStrings.PackageWithNoAssertionAndPurlJsonString; + expectedJsonContentAsString = NormalizeString(expectedJsonContentAsString); + + Assert.IsFalse(generatedJsonString.Contains("null")); + Assert.AreEqual(generatedJsonString, expectedJsonContentAsString); + } + + [TestMethod] + public void GenerateJsonDocumentTest_File() + { + var fileInfo = new InternalSbomFileInfo + { + Checksum = GetSampleChecksums(), + FileCopyrightText = "sampleCopyright", + LicenseConcluded = "sampleLicense1", + LicenseInfoInFiles = new List { "sampleLicense1" }, + Path = "/sample/path", + }; + + var generatorResult = generator.GenerateJsonDocument(fileInfo); + var generatedJsonString = generatorResult.Document.RootElement.GetRawText(); + generatedJsonString = NormalizeString(generatedJsonString); + + var expectedJsonContentAsString = SbomFileJsonStrings.FileWithLicensesAndHashes; + expectedJsonContentAsString = NormalizeString(expectedJsonContentAsString); + + Assert.IsFalse(generatedJsonString.Contains("null")); + Assert.AreEqual(generatedJsonString, expectedJsonContentAsString); + } + + [TestMethod] + public void GenerateJsonDocumentTest_ExternalMap() + { + var externalDocInfo = new ExternalDocumentReferenceInfo + { + DocumentNamespace = "sample-namespace", + Checksum = GetSampleChecksums(), + ExternalDocumentName = "sample-external-doc", + }; + + var generatorResult = generator.GenerateJsonDocument(externalDocInfo); + var generatedJsonString = generatorResult.Document.RootElement.GetRawText(); + generatedJsonString = NormalizeString(generatedJsonString); + + var expectedJsonContentAsString = SbomExternalMapJsonStrings.ExternalMapJsonString; + expectedJsonContentAsString = NormalizeString(expectedJsonContentAsString); + + Assert.IsFalse(generatedJsonString.Contains("null")); + Assert.AreEqual(generatedJsonString, expectedJsonContentAsString); + } + + [TestMethod] + public void GenerateJsonDocumentTest_Relationship() + { + var relationshipInfo = new Relationship + { + SourceElementId = "source-id", + TargetElementId = "target-id", + RelationshipType = RelationshipType.DESCRIBES, + }; + + var generatorResult = generator.GenerateJsonDocument(relationshipInfo); + var generatedJsonString = generatorResult.Document.RootElement.GetRawText(); + generatedJsonString = NormalizeString(generatedJsonString); + + var expectedJsonContentAsString = SbomRelationshipJsonStrings.RelationshipJsonString; + expectedJsonContentAsString = NormalizeString(expectedJsonContentAsString); + + Assert.IsFalse(generatedJsonString.Contains("null")); + Assert.AreEqual(generatedJsonString, expectedJsonContentAsString); + } + + [TestCleanup] + public void Cleanup() + { + recorderMock.VerifyAll(); + mockLogger.VerifyAll(); + fileSystemMock.VerifyAll(); + mockConfigHandler.VerifyAll(); + } + + private string NormalizeString(string input) + { + return input.Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } + + private string ConvertJsonToRegex(string input) + { + var pattern = Regex.Escape(input); + + // Replace placeholders with appropriate regex patterns + pattern = pattern.Replace(@"\.\*", ".*"); + return pattern; + } + + private List GetSampleChecksums() + { + return new List + { + new Checksum + { + Algorithm = AlgorithmName.SHA1, + ChecksumValue = "sha1Value" + }, new Checksum + { + Algorithm = AlgorithmName.SHA256, + ChecksumValue = "sha256Value" + }, + }; + } + + private IInternalMetadataProvider CreateInternalMetadataProvider() + { + ISbomConfig sbomConfig = new SbomConfig(fileSystemMock.Object) + { + ManifestInfo = Constants.Spdx30ManifestInfo, + Recorder = new SbomPackageDetailsRecorder() + }; + + mockConfigHandler.Setup(c => c.TryGetManifestConfig(out sbomConfig)).Returns(true); + recorderMock.Setup(r => r.RecordSBOMFormat(Constants.Spdx30ManifestInfo, It.IsAny())); + mockLogger.Setup(l => l.Debug(It.IsAny())); + + var config = new Configuration + { + PackageName = new ConfigurationSetting("the-package-name"), + PackageVersion = new ConfigurationSetting("the-package-version"), + NamespaceUriUniquePart = new ConfigurationSetting("some-custom-value-here"), + NamespaceUriBase = new ConfigurationSetting("http://sbom.microsoft"), + PackageSupplier = new ConfigurationSetting("the-package-supplier"), + }; + + var sbomMetadata = new SBOMMetadata + { + PackageName = "sbom-package-name", + PackageVersion = "sbom-package-version", + BuildEnvironmentName = "the-build-envsdfgsdg", + }; + + var localMetadataProvider = new LocalMetadataProvider(config); + + var sbomApiMetadataProvider = new SBOMApiMetadataProvider(sbomMetadata, config); + var metadataProviders = new IMetadataProvider[] { localMetadataProvider, sbomApiMetadataProvider }; + IInternalMetadataProvider sbomConfigs = CreateSbomConfigs(metadataProviders); + + return sbomConfigs; + } + + private ISbomConfigProvider CreateSbomConfigs(IMetadataProvider[] metadataProviders) => + new SbomConfigProvider( + manifestConfigHandlers: new IManifestConfigHandler[] { mockConfigHandler.Object }, + metadataProviders: metadataProviders, + logger: mockLogger.Object, + recorder: recorderMock.Object); +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomDocCreationJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomDocCreationJsonStrings.cs new file mode 100644 index 00000000..1394e2c4 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomDocCreationJsonStrings.cs @@ -0,0 +1,74 @@ +// 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.Parser.JsonStrings; + +public static class SbomDocCreationJsonStrings +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string DocCreationJsonString = + @"[ + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""the-package-supplier"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-.*"", + ""spdxId"": ""SPDXRef-Tool-.*"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": "".*"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-.*"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""CC0-1.0"", + ""spdxId"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""type"": ""AnyLicenseInfo"" + }, + { + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""namespaceMap"": [ + { + ""namespace"": ""http://sbom.microsoft/sbom-package-name/sbom-package-version/some-custom-value-here"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-NamespaceMap-0C5D68EB49795A98E060EB263AC73F87322217857EB3057EBAC84A70F75E69BE"", + ""type"": ""NamespaceMap"" + } + ], + ""profileConformance"": [ + ""software"", + ""core"", + ""simpleLicensing"" + ], + ""creationInfo"": ""_:creationinfo"", + ""name"": ""the-package-namethe-package-version"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + }, + { + ""from"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""relationshipType"": ""HAS_DECLARED_LICENSE"", + ""to"": [ + ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"" + ], + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-Relationship-222E099165617B282F2B424519FC133796AA0189D0238FD121CCF3B0340D4301"", + ""type"": ""Relationship"" + } + ]"; +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomExternalMapJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomExternalMapJsonStrings.cs new file mode 100644 index 00000000..ba5db4d0 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomExternalMapJsonStrings.cs @@ -0,0 +1,26 @@ +// 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.Parser.JsonStrings; + +public static class SbomExternalMapJsonStrings +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string ExternalMapJsonString = + @" + { + ""externalSpdxId"": ""DocumentRef-sample-external-doc-sha1Value"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-ExternalMap-2AB84930C880E9632BC8FCFBC0BA22D66E2912252D3457ACB89A9DDE4745E2D0"", + ""verifiedUsing"": [ + { + ""algorithm"": ""sha1"", + ""hashValue"": ""sha1value"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-PackageVerificationCode-B1565820A5CDAC40E0520D23F9D0B1497F240DDC51D72EAC6423D97D952D444F"", + ""type"": ""PackageVerificationCode"" + } + ], + ""type"": ""ExternalMap"" + }"; +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFileJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFileJsonStrings.cs new file mode 100644 index 00000000..c05bc2a9 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFileJsonStrings.cs @@ -0,0 +1,61 @@ +// 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.Parser.JsonStrings; + +public static class SbomFileJsonStrings +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string FileWithLicensesAndHashes = + @"[ + { + ""CopyrightText"": ""sampleCopyright"", + ""creationInfo"": ""_:creationinfo"", + ""name"": ""./sample/path"", + ""spdxId"": ""SPDXRef-software_File-B4A9F99A3A03B9273AE34753D96564CB4F2B0FAD885BBD36B0DD619E9E8AC967"", + ""verifiedUsing"": [ + { + ""algorithm"": ""sha1"", + ""hashValue"": ""sha1value"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-PackageVerificationCode-B1565820A5CDAC40E0520D23F9D0B1497F240DDC51D72EAC6423D97D952D444F"", + ""type"": ""PackageVerificationCode"" + }, + { + ""algorithm"": ""sha256"", + ""hashValue"": ""sha256value"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-PackageVerificationCode-5D5B09F6DCB2D53A5FFFC60C4AC0D55FABDF556069D6631545F42AA6E3500F2E"", + ""type"": ""PackageVerificationCode"" + } + ], + ""type"": ""software_File"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""sampleLicense1"", + ""spdxId"": ""SPDXRef-AnyLicenseInfo-3BA3FA6D3D66FE2BA75992BB0850D080F1223256368A76C77BEF8E0F6AC71896"", + ""type"": ""AnyLicenseInfo"" + }, + { + ""from"": ""SPDXRef-software_File-B4A9F99A3A03B9273AE34753D96564CB4F2B0FAD885BBD36B0DD619E9E8AC967"", + ""relationshipType"": ""HAS_CONCLUDED_LICENSE"", + ""to"": [ + ""SPDXRef-AnyLicenseInfo-3BA3FA6D3D66FE2BA75992BB0850D080F1223256368A76C77BEF8E0F6AC71896"" + ], + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-Relationship-2A69D50DCB6D763C5C4FFCE6A4F6C3166DD8F6DB3F77BDD4B6129C0B33F238DA"", + ""type"": ""Relationship"" + }, + { + ""from"": ""SPDXRef-software_File-B4A9F99A3A03B9273AE34753D96564CB4F2B0FAD885BBD36B0DD619E9E8AC967"", + ""relationshipType"": ""HAS_DECLARED_LICENSE"", + ""to"": [ + ""SPDXRef-AnyLicenseInfo-3BA3FA6D3D66FE2BA75992BB0850D080F1223256368A76C77BEF8E0F6AC71896"" + ], + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-Relationship-222E099165617B282F2B424519FC133796AA0189D0238FD121CCF3B0340D4301"", + ""type"": ""Relationship"" + } + ]"; +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomPackageJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomPackageJsonStrings.cs new file mode 100644 index 00000000..15f89746 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomPackageJsonStrings.cs @@ -0,0 +1,134 @@ +// 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.Parser.JsonStrings; + +public static class SbomPackageJsonStrings +{ + public const string PackageWithNoAssertionAndPurlJsonString = + /*lang=json,strict*/ + @"[ + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""NOASSERTION"", + ""spdxId"": ""SPDXRef-Organization-8560FC6692684D8DF52223FF78E30B9630A1CF5A6FA371AAE24FCA896AE20969"", + ""type"": ""Organization"" + }, + { + ""suppliedBy"": ""SPDXRef-Organization-8560FC6692684D8DF52223FF78E30B9630A1CF5A6FA371AAE24FCA896AE20969"", + ""CopyrightText"": ""NOASSERTION"", + ""software_downloadLocation"": ""NOASSERTION"", + ""creationInfo"": ""_:creationinfo"", + ""externalIdentifier"": [ + ""SPDXRef-ExternalIdentifier-CE6B7E4A59503594D0AF89492494E05071399F36D9085F1E722497D78A9404E1"" + ], + ""name"": ""test"", + ""spdxId"": ""SPDXRef-software_Package-4739C82D88855A138C811B8CE05CC97113BEC7F7C7F66EC7E4C6C176EEA0FECE"", + ""type"": ""software_Package"" + }, + { + ""externalIdentifierType"": ""purl"", + ""identifier"": ""packageUrl"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-ExternalIdentifier-CE6B7E4A59503594D0AF89492494E05071399F36D9085F1E722497D78A9404E1"", + ""type"": ""ExternalIdentifier"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""NoAssertion"", + ""spdxId"": ""SPDXRef-Element-D6D57C0C9CC2CAC35C83DE0C8E4C8C37B87C0A58DA49BB31EBEBC6E200F54D4B"", + ""type"": ""Element"" + }, + { + ""from"": ""SPDXRef-software_Package-4739C82D88855A138C811B8CE05CC97113BEC7F7C7F66EC7E4C6C176EEA0FECE"", + ""relationshipType"": ""HAS_CONCLUDED_LICENSE"", + ""to"": [ + ""SPDXRef-Element-D6D57C0C9CC2CAC35C83DE0C8E4C8C37B87C0A58DA49BB31EBEBC6E200F54D4B"" + ], + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-Relationship-2A69D50DCB6D763C5C4FFCE6A4F6C3166DD8F6DB3F77BDD4B6129C0B33F238DA"", + ""type"": ""Relationship"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""NoAssertion"", + ""spdxId"": ""SPDXRef-Element-D6D57C0C9CC2CAC35C83DE0C8E4C8C37B87C0A58DA49BB31EBEBC6E200F54D4B"", + ""type"": ""Element"" + }, + { + ""from"": ""SPDXRef-software_Package-4739C82D88855A138C811B8CE05CC97113BEC7F7C7F66EC7E4C6C176EEA0FECE"", + ""relationshipType"": ""HAS_DECLARED_LICENSE"", + ""to"": [ + ""SPDXRef-Element-D6D57C0C9CC2CAC35C83DE0C8E4C8C37B87C0A58DA49BB31EBEBC6E200F54D4B"" + ], + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-Relationship-222E099165617B282F2B424519FC133796AA0189D0238FD121CCF3B0340D4301"", + ""type"": ""Relationship"" + } + ]"; + + public const string RootPackageJsonString = + /*lang=json,strict*/ + @"[ + { + ""externalIdentifierType"": ""purl"", + ""identifier"": ""pkg:swid/the-package-supplier/sbom.microsoft/the-package-name@the-package-version?tag_id=.*"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-ExternalIdentifier-.*"", + ""type"": ""ExternalIdentifier"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Organization: the-package-supplier"", + ""spdxId"": ""SPDXRef-Organization-D914F48404CB6C27373666F70709BABF08C6603E1303B97758A20190A050CE16"", + ""type"": ""Organization"" + }, + { + ""CopyrightText"": ""NOASSERTION"", + ""software_downloadLocation"": ""NOASSERTION"", + ""software_packageVersion"": ""the-package-version"", + ""creationInfo"": ""_:creationinfo"", + ""externalIdentifier"": [ + ""SPDXRef-ExternalIdentifier-.*"" + ], + ""name"": ""the-package-name"", + ""spdxId"": ""SPDXRef-software_Package-A8C9BE15D102D0AF9D34A9EAA6FE282AB0CE35FF48A83058703A964E512B68B7"", + ""verifiedUsing"": [ + { + ""algorithm"": ""sha1"", + ""hashValue"": ""da39a3ee5e6b4b0d3255bfef95601890afd80709"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-PackageVerificationCode-B1565820A5CDAC40E0520D23F9D0B1497F240DDC51D72EAC6423D97D952D444F"", + ""type"": ""PackageVerificationCode"" + } + ], + ""type"": ""software_Package"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""NoAssertion"", + ""spdxId"": ""SPDXRef-Element-D6D57C0C9CC2CAC35C83DE0C8E4C8C37B87C0A58DA49BB31EBEBC6E200F54D4B"", + ""type"": ""Element"" + }, + { + ""from"": ""SPDXRef-RootPackage"", + ""relationshipType"": ""HAS_DECLARED_LICENSE"", + ""to"": [ + ""SPDXRef-Element-D6D57C0C9CC2CAC35C83DE0C8E4C8C37B87C0A58DA49BB31EBEBC6E200F54D4B"" + ], + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-Relationship-222E099165617B282F2B424519FC133796AA0189D0238FD121CCF3B0340D4301"", + ""type"": ""Relationship"" + }, + { + ""from"": ""SPDXRef-RootPackage"", + ""relationshipType"": ""HAS_CONCLUDED_LICENSE"", + ""to"": [ + ""SPDXRef-Element-D6D57C0C9CC2CAC35C83DE0C8E4C8C37B87C0A58DA49BB31EBEBC6E200F54D4B"" + ], + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-Relationship-2A69D50DCB6D763C5C4FFCE6A4F6C3166DD8F6DB3F77BDD4B6129C0B33F238DA"", + ""type"": ""Relationship"" + } + ]"; +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomRelationshipJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomRelationshipJsonStrings.cs new file mode 100644 index 00000000..728d2d37 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomRelationshipJsonStrings.cs @@ -0,0 +1,22 @@ +// 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.Parser.JsonStrings; + +public static class SbomRelationshipJsonStrings +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string RelationshipJsonString = + @" + { + ""from"": ""source-id"", + ""relationshipType"": ""DESCRIBES"", + ""to"": [ + ""target-id"" + ], + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-Relationship-4A4165C4543F807E438AC18B09585043D3F49095D996BC098C74EC358AD24558"", + ""type"": ""Relationship"" + } + "; +}