diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/Constants.cs b/src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/Constants.cs similarity index 100% rename from src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/Constants.cs rename to src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/Constants.cs diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/Exceptions/ParserException.cs b/src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/Exceptions/ParserException.cs similarity index 100% rename from src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/Exceptions/ParserException.cs rename to src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/Exceptions/ParserException.cs diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/LargeJsonParser.cs b/src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/LargeJsonParser.cs similarity index 94% rename from src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/LargeJsonParser.cs rename to src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/LargeJsonParser.cs index 4815b0f8..5794010f 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/LargeJsonParser.cs +++ b/src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/LargeJsonParser.cs @@ -21,7 +21,7 @@ namespace Microsoft.Sbom.JsonAsynchronousNodeKit; /// This class is not Thread-safe since the stream and JsonReaders assume a single forward-only reader. /// Because of the use of recursion in the GetObject method, this class is also not suitable for parsing very deep json objects. /// -internal class LargeJsonParser +public class LargeJsonParser { private const int DefaultReadBufferSize = 4096; private readonly Stream stream; @@ -33,18 +33,20 @@ internal class LargeJsonParser private bool isParsingStarted = false; private bool enumeratorActive = false; private ParserStateResult? previousResultState = null; + private bool isSpdx30 = false; public LargeJsonParser( Stream stream, IReadOnlyDictionary handlers, JsonSerializerOptions? jsonSerializerOptions = default, - int bufferSize = DefaultReadBufferSize) + int bufferSize = DefaultReadBufferSize, + bool isSpdx30 = false) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); this.handlers = handlers ?? throw new ArgumentNullException(nameof(handlers)); this.jsonSerializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions(); - this.buffer = new byte[bufferSize]; + this.isSpdx30 = isSpdx30; // Validate buffer is not of 0 length. if (this.buffer.Length == 0) @@ -153,6 +155,7 @@ private ParserStateResult HandleExplicitProperty(ref Utf8JsonReader reader, stri var handler = this.handlers![propertyName]; object? result; + switch (handler.Type) { case ParameterType.String: @@ -268,11 +271,23 @@ private IEnumerable GetArray(Type type) private object GetObject(Type type, ref Utf8JsonReader reader) { - var jsonObject = ParserUtils.ParseObject(this.stream, ref this.buffer, ref reader); - object? result = jsonObject; - if (type != typeof(JsonNode)) + object? result = null; + + if (type == typeof(string)) + { + result = reader.GetString(); + } + else { - result = jsonObject.Deserialize(type, this.jsonSerializerOptions); + var jsonObject = ParserUtils.ParseObject(this.stream, ref this.buffer, ref reader); + if (type != typeof(JsonNode)) + { + result = jsonObject.Deserialize(type, this.jsonSerializerOptions); + } + else + { + result = jsonObject; + } } if (result is null) diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/ParameterType.cs b/src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/ParameterType.cs similarity index 100% rename from src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/ParameterType.cs rename to src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/ParameterType.cs diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/ParserUtils.cs b/src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/ParserUtils.cs similarity index 100% rename from src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/ParserUtils.cs rename to src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/ParserUtils.cs diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/PropertyHandler.cs b/src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/PropertyHandler.cs similarity index 100% rename from src/Microsoft.Sbom.Parsers.Spdx22SbomParser/JsonAsynchronousNodeKit/PropertyHandler.cs rename to src/Microsoft.Sbom.Common/JsonAsynchronousNodeKit/PropertyHandler.cs diff --git a/src/Microsoft.Sbom.Contracts/Contracts/Spdx22Metadata.cs b/src/Microsoft.Sbom.Contracts/Contracts/SpdxMetadata.cs similarity index 97% rename from src/Microsoft.Sbom.Contracts/Contracts/Spdx22Metadata.cs rename to src/Microsoft.Sbom.Contracts/Contracts/SpdxMetadata.cs index b627c727..51985296 100644 --- a/src/Microsoft.Sbom.Contracts/Contracts/Spdx22Metadata.cs +++ b/src/Microsoft.Sbom.Contracts/Contracts/SpdxMetadata.cs @@ -9,7 +9,7 @@ namespace Microsoft.Sbom.Contracts; /// /// The object representation of all the metadata in an SPDX document. /// -public class Spdx22Metadata +public class SpdxMetadata { /// /// The version of the SPDX specification used in this document. diff --git a/src/Microsoft.Sbom.Extensions/ISbomParser.cs b/src/Microsoft.Sbom.Extensions/ISbomParser.cs index 243daa2e..8db84968 100644 --- a/src/Microsoft.Sbom.Extensions/ISbomParser.cs +++ b/src/Microsoft.Sbom.Extensions/ISbomParser.cs @@ -27,7 +27,7 @@ public interface ISbomParser /// /// /// - Spdx22Metadata GetMetadata(); + SpdxMetadata GetMetadata(); /// /// This function is called by the sbom tool upon initialization to get all the diff --git a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Parser/SPDXParser.cs b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Parser/SPDXParser.cs index 57c71771..53a4b248 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Parser/SPDXParser.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx22SbomParser/Parser/SPDXParser.cs @@ -172,14 +172,14 @@ internal SPDXParser( return null; } - public Spdx22Metadata GetMetadata() + public SpdxMetadata GetMetadata() { if (!this.parsingComplete) { throw new ParserException($"{nameof(this.GetMetadata)} can only be called after Parsing is complete to ensure that a whole object is returned."); } - var spdxMetadata = new Spdx22Metadata(); + var spdxMetadata = new SpdxMetadata(); foreach (var kvp in this.metadata) { switch (kvp.Key) diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Constants.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Constants.cs index 6b460cb6..8f3a3869 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Constants.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Constants.cs @@ -16,6 +16,10 @@ internal static class Constants internal const string SPDXDocumentNameFormatString = "{0} {1}"; internal const string PackageSupplierFormatString = "Organization: {0}"; + internal const string SPDXVersionHeaderName = "specVersion"; + internal const string SPDXContextHeaderName = "@context"; + internal const string SPDXGraphHeaderName = "@graph"; + /// /// Use if SPDX creator /// - made an attempt to retrieve the info but cannot determine correct values. @@ -24,6 +28,8 @@ internal static class Constants /// internal const string NoAssertionValue = "NOASSERTION"; + internal const int ReadBufferSize = 4096; + /// /// The value as a list with a single item. /// diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Element.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Element.cs index 8c33f930..4153a4ab 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Element.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Element.cs @@ -45,7 +45,7 @@ protected Element() [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("name")] - public string Name { get; set; } + public virtual string Name { get; set; } /// /// Gets or sets unique Identifier for elements in SPDX document. @@ -63,7 +63,7 @@ protected Element() /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("verifiedUsing")] - public List VerifiedUsing { get; set; } + public virtual List VerifiedUsing { get; set; } [JsonRequired] [JsonPropertyName("type")] diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/RelationshipType.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/RelationshipType.cs index ce212716..9f0a3b8b 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/RelationshipType.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Enums/RelationshipType.cs @@ -130,7 +130,7 @@ public enum RelationshipType /// /// 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 is an artifact that stores data required or optional for the from Element'ContextsResult 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. @@ -284,7 +284,7 @@ public enum RelationshipType REPORTED_BY, /// - /// Designates a from Vulnerability's details were tracked, aggregated, and/or enriched to improve context (i.e. NVD) by each to Agent. + /// Designates a from Vulnerability'ContextsResult details were tracked, aggregated, and/or enriched to improve context (i.e. NVD) by each to Agent. /// REPUBLISHED_BY, diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/File.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/File.cs index 6b03da44..8667a151 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/File.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/File.cs @@ -22,4 +22,8 @@ public File() [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("mediaType")] public object MediaType { get; set; } + + [JsonRequired] + [JsonPropertyName("name")] + public override string Name { get; set; } } diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/FormatEnforcedSPDX3.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/FormatEnforcedSPDX3.cs index 3a7b1466..2168c5a4 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/FormatEnforcedSPDX3.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/FormatEnforcedSPDX3.cs @@ -25,7 +25,7 @@ public class FormatEnforcedSPDX3 /// [JsonRequired] [JsonPropertyName("@graph")] - public List Graph { get; set; } + public IEnumerable Graph { get; set; } /// /// Required as part of the Core profile. diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NTIAFile.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NTIAFile.cs new file mode 100644 index 00000000..44572c50 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NTIAFile.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; +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/ +/// An NTIA file specifically describes a file compliant with the NTIA SBOM standard. +/// +public class NTIAFile : File +{ + public NTIAFile() + { + Type = "software_File"; + } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("software_contentType")] + public object ContentType { get; set; } + + /// + /// Make verification code required for Files. This is an internal requirement, not a requirement from SPDX. + /// + [JsonRequired] + [JsonPropertyName("verifiedUsing")] + public override List VerifiedUsing { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NTIASpdxDocument.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NTIASpdxDocument.cs new file mode 100644 index 00000000..aa45654a --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NTIASpdxDocument.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.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/ +/// An NTIA SpdxDocument specifically describes a SpdxDocument entity compliant with the NTIA SBOM standard. +/// +public class NTIASpdxDocument : SpdxDocument +{ + public NTIASpdxDocument() + { + Type = nameof(SpdxDocument); + } + + [JsonRequired] + [JsonPropertyName("name")] + public override string Name { get; set; } +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NamespaceMap.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NamespaceMap.cs deleted file mode 100644 index eb53a324..00000000 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/NamespaceMap.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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/Package.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Package.cs index a0a95f94..091df823 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Package.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Package.cs @@ -22,5 +22,9 @@ public Package() /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("suppliedBy")] - public string SuppliedBy { get; set; } + public virtual string SuppliedBy { get; set; } + + [JsonRequired] + [JsonPropertyName("name")] + public override string Name { get; set; } } diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Spdx30Relationship.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Relationship.cs similarity index 91% rename from src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Spdx30Relationship.cs rename to src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Relationship.cs index 974e8940..34309e10 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Spdx30Relationship.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Relationship.cs @@ -13,12 +13,12 @@ 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 +public class Relationship : Element { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public Spdx30Relationship() + public Relationship() { Type = nameof(Relationship); } diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Software.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Software.cs index a2e2ed0a..a2611bee 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Software.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/Software.cs @@ -4,8 +4,6 @@ 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/ @@ -37,8 +35,8 @@ public abstract class Software : Element public string ContentIdentifierValue { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonProperty(PropertyName = "software_copyrightText")] - public string CopyrightText { get; set; } + [JsonPropertyName("software_copyrightText")] + public virtual string CopyrightText { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("software_downloadLocation")] @@ -62,7 +60,7 @@ public abstract class Software : Element [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("software_packageVersion")] - public string PackageVersion { get; set; } + public virtual string PackageVersion { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("software_primaryPurpose")] @@ -77,6 +75,6 @@ public abstract class Software : Element public File SnippetFromFile { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("software_copyrightText")] + [JsonPropertyName("software_sourceInfo")] public string SourceInfo { get; set; } } diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/SpdxDocument.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/SpdxDocument.cs index 04284f74..27537dd6 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/SpdxDocument.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Entities/SpdxDocument.cs @@ -23,7 +23,7 @@ public class SpdxDocument : Element [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("namespaceMap")] - public List NamespaceMap { get; set; } + public Dictionary NamespaceMap { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("element")] @@ -35,5 +35,5 @@ public class SpdxDocument : Element [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("rootElement")] - public List RootElement { get; set; } + public List RootElement { get; set; } } diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Generator.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Generator.cs index 4b61b0cb..660c6c53 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Generator.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Generator.cs @@ -3,22 +3,14 @@ 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; @@ -200,14 +192,14 @@ public GenerationResult GenerateRootPackage(IInternalMetadataProvider internalMe // Generate SPDX relationship elements to indicate no assertions are made about licenses for this root package. var noAssertionLicense = GenerateLicenseElement(null); - var spdxRelationshipLicenseDeclaredElement = new Spdx30Relationship + var spdxRelationshipLicenseDeclaredElement = new Entities.Relationship { From = spdxPackage.SpdxId, RelationshipType = RelationshipType.HAS_DECLARED_LICENSE, To = new List { noAssertionLicense.SpdxId }, }; - var spdxRelationshipLicenseConcludedElement = new Spdx30Relationship + var spdxRelationshipLicenseConcludedElement = new Entities.Relationship { From = spdxPackage.SpdxId, RelationshipType = RelationshipType.HAS_CONCLUDED_LICENSE, @@ -300,7 +292,7 @@ public GenerationResult GenerateJsonDocument(ExternalDocumentReferenceInfo exter /// Relationship info /// /// - public GenerationResult GenerateJsonDocument(Relationship relationship) + public GenerationResult GenerateJsonDocument(Extensions.Entities.Relationship relationship) { if (relationship is null) { @@ -314,7 +306,7 @@ public GenerationResult GenerateJsonDocument(Relationship relationship) : relationship.TargetElementId; var sourceElement = relationship.SourceElementId; - var spdxRelationship = new Spdx30Relationship + var spdxRelationship = new Entities.Relationship { From = sourceElement, RelationshipType = this.GetSPDXRelationshipType(relationship.RelationshipType), @@ -374,12 +366,8 @@ public GenerationResult GenerateJsonDocument(IInternalMetadataProvider internalM CreatedUsing = new List { spdxTool.SpdxId }, }; - var spdxNamespaceMap = new NamespaceMap - { - Namespace = internalMetadataProvider.GetDocumentNamespace(), - }; - - spdxNamespaceMap.AddSpdxId(); + var spdxNamespaceMap = new Dictionary(); + spdxNamespaceMap["sbom"] = internalMetadataProvider.GetDocumentNamespace(); var spdxDataLicense = new AnyLicenseInfo { @@ -390,7 +378,7 @@ public GenerationResult GenerateJsonDocument(IInternalMetadataProvider internalM var spdxDocument = new SpdxDocument { DataLicense = spdxDataLicense.SpdxId, - NamespaceMap = new List { spdxNamespaceMap }, + NamespaceMap = spdxNamespaceMap, SpdxId = Constants.SPDXDocumentIdValue, Name = documentName, ProfileConformance = new List { ProfileIdentifierType.software, ProfileIdentifierType.core, ProfileIdentifierType.simpleLicensing }, @@ -398,7 +386,7 @@ public GenerationResult GenerateJsonDocument(IInternalMetadataProvider internalM spdxDocument.AddSpdxId(); - var spdxRelationship = new Spdx30Relationship + var spdxRelationship = new Entities.Relationship { From = spdxDataLicense.SpdxId, RelationshipType = RelationshipType.HAS_DECLARED_LICENSE, @@ -485,7 +473,7 @@ private List GetSpdxRelationshipsFromSbomFile(Element spdxFileElement, var licenseConcludedElement = GenerateLicenseElement(fileInfo.LicenseConcluded); spdxRelationshipAndLicenseElementsToAddToSBOM.Add(licenseConcludedElement); - var spdxRelationshipLicenseConcludedElement = new Spdx30Relationship + var spdxRelationshipLicenseConcludedElement = new Entities.Relationship { From = spdxFileElement.SpdxId, RelationshipType = RelationshipType.HAS_CONCLUDED_LICENSE, @@ -503,7 +491,7 @@ private List GetSpdxRelationshipsFromSbomFile(Element spdxFileElement, toRelationships.Add(licenseDeclaredElement.SpdxId); } - var spdxRelationshipLicenseDeclaredElement = new Spdx30Relationship + var spdxRelationshipLicenseDeclaredElement = new Entities.Relationship { From = spdxFileElement.SpdxId, RelationshipType = RelationshipType.HAS_DECLARED_LICENSE, @@ -524,7 +512,7 @@ private List GetSpdxRelationshipsAndLicensesFromSbomPackage(SbomPackage var licenseConcludedElement = GenerateLicenseElement(packageInfo.LicenseInfo?.Concluded); spdxRelationshipAndLicenseElementsToAddToSBOM.Add(licenseConcludedElement); - var spdxRelationshipLicenseConcludedElement = new Spdx30Relationship + var spdxRelationshipLicenseConcludedElement = new Entities.Relationship { From = spdxPackage.SpdxId, RelationshipType = RelationshipType.HAS_CONCLUDED_LICENSE, @@ -538,7 +526,7 @@ private List GetSpdxRelationshipsAndLicensesFromSbomPackage(SbomPackage var licenseDeclaredElement = GenerateLicenseElement(packageInfo.LicenseInfo?.Declared); spdxRelationshipAndLicenseElementsToAddToSBOM.Add(licenseDeclaredElement); - var spdxRelationshipLicenseDeclaredElement = new Spdx30Relationship + var spdxRelationshipLicenseDeclaredElement = new Entities.Relationship { From = spdxPackage.SpdxId, RelationshipType = RelationshipType.HAS_DECLARED_LICENSE, diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ContextsResult.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ContextsResult.cs new file mode 100644 index 00000000..67f35068 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ContextsResult.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Sbom.JsonAsynchronousNodeKit; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +namespace Microsoft.Sbom.Parser; + +public record ContextsResult : ParserStateResult +{ + public ContextsResult(ParserStateResult result) + : base(result.FieldName, result.Result, result.ExplicitField, result.YieldReturn) + { + } + + public IEnumerable Contexts => ((IEnumerable)this.Result!).Select(r => r); +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ElementsResult.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ElementsResult.cs new file mode 100644 index 00000000..e9df538c --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ElementsResult.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Sbom.JsonAsynchronousNodeKit; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +namespace Microsoft.Sbom.Parser; + +public record ElementsResult : ParserStateResult +{ + public ElementsResult(ParserStateResult result) + : base(result.FieldName, result.Result, result.ExplicitField, result.YieldReturn) + { + } + + public IEnumerable Elements => ((IEnumerable)this.Result!).Select(r => (Element)r); +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ParserResults.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ParserResults.cs new file mode 100644 index 00000000..a932318a --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/ParserResults.cs @@ -0,0 +1,17 @@ +// 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; + +using System.Collections.Generic; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +public class ParserResults +{ + public FormatEnforcedSPDX3 FormatEnforcedSPDX3Result { get; set; } + + public int FilesCount = 0; + public int PackagesCount = 0; + public int ReferencesCount = 0; + public int RelationshipsCount = 0; +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/SPDX30Parser.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/SPDX30Parser.cs new file mode 100644 index 00000000..e03d5e13 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Parser/SPDX30Parser.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Sbom.Contracts; +using Microsoft.Sbom.Extensions.Entities; +using Microsoft.Sbom.JsonAsynchronousNodeKit; +using Microsoft.Sbom.JsonAsynchronousNodeKit.Exceptions; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; +using SPDXConstants = Microsoft.Sbom.Parsers.Spdx30SbomParser.Constants; + +namespace Microsoft.Sbom.Parser; + +#nullable enable +/// +/// A parser for SPDX 3.0 SBOMs. +/// +/// +/// This class is not Thread-safe since the stream and JsonReaders assume a single forward-only reader. +/// Because of the use of recursion in , this class is also not suitable for parsing deeply nested json objects. +/// +public class SPDX30Parser : ISbomParser +{ + public const string ContextProperty = SPDXConstants.SPDXContextHeaderName; + public const string GraphProperty = SPDXConstants.SPDXGraphHeaderName; + public static readonly IReadOnlyCollection RequiredFields = new List + { + ContextProperty, + GraphProperty, + }; + + public string? RequiredComplianceStandard; + public IReadOnlyCollection? EntitiesToEnforceComplianceStandardsFor; + public SpdxMetadata Metadata = new SpdxMetadata(); + private readonly LargeJsonParser parser; + private readonly IList observedFieldNames = new List(); + private readonly bool requiredFieldsCheck = true; + private readonly JsonSerializerOptions jsonSerializerOptions; + private bool parsingComplete = false; + private readonly ManifestInfo spdxManifestInfo = new() + { + Name = SPDXConstants.SPDXName, + Version = SPDXConstants.SPDXVersion, + }; + + private readonly IReadOnlyCollection entitiesWithDifferentNTIARequirements = new List + { + "SpdxDocument", + "File", + "Package", + }; + + public SPDX30Parser( + Stream stream, + string? requiredComplianceStandard = null, + JsonSerializerOptions? jsonSerializerOptions = null, + int? bufferSize = null) + { + this.jsonSerializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions + { + Converters = { new ElementSerializer() }, + }; + + var handlers = new Dictionary + { + { ContextProperty, new PropertyHandler(ParameterType.Array) }, + { GraphProperty, new PropertyHandler(ParameterType.Array) }, + }; + + switch (requiredComplianceStandard) + { + case "NTIA": + this.EntitiesToEnforceComplianceStandardsFor = this.entitiesWithDifferentNTIARequirements; + this.RequiredComplianceStandard = requiredComplianceStandard; + break; + } + + if (bufferSize is null) + { + this.parser = new LargeJsonParser(stream, handlers, this.jsonSerializerOptions, isSpdx30: true); + } + else + { + this.parser = new LargeJsonParser(stream, handlers, this.jsonSerializerOptions, bufferSize.Value, isSpdx30: true); + } + } + + /// + /// Return the result from the parser. + /// + /// null if parsing is complete, otherwise a representing the field which was visited. These results represent the root level fields of a json object. + /// If the field is an array the result will be an IEnumerable which you MUST fully enumerate before calling Next() again. + /// + /// + public ParserStateResult? Next() + { + ParserStateResult? result; + do + { + result = this.parser.Next(); + if (result is not null) + { + this.observedFieldNames.Add(result.FieldName); + if (result.Result is not null) + { + switch (result.FieldName) + { + case ContextProperty: + result = new ContextsResult(result); + break; + case GraphProperty: + result = new ElementsResult(result); + break; + default: + throw new InvalidDataException($"Explicit field {result.FieldName} is unhandled."); + } + + return result; + } + } + } + while (result is not null); + + if (this.requiredFieldsCheck) + { + foreach (var requiredField in RequiredFields) + { + if (!this.observedFieldNames.Contains(requiredField)) + { + throw new ParserException($"Required field {requiredField} was not found in the SPDX file"); + } + } + } + + this.parsingComplete = true; + + return null; + } + + public SpdxMetadata GetMetadata() + { + if (!this.parsingComplete) + { + throw new ParserException($"{nameof(this.GetMetadata)} can only be called after Parsing is complete to ensure that a whole object is returned."); + } + + return this.Metadata; + } + + public ManifestInfo[] RegisterManifest() => new ManifestInfo[] { this.spdxManifestInfo }; +} diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/ElementSerializer.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/ElementSerializer.cs index c47af346..f6c06f8b 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/ElementSerializer.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/ElementSerializer.cs @@ -5,15 +5,12 @@ 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) - { + 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) { diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/SPDXExtensions.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/SPDXExtensions.cs index 0b8cfbfc..312d4a65 100644 --- a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/SPDXExtensions.cs +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Utils/SPDXExtensions.cs @@ -124,17 +124,12 @@ 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) + public static void AddSpdxId(this Entities.Relationship relationship) { relationship.SpdxId = GenerateSpdxId(relationship, relationship.To + relationship.RelationshipType.ToString()); } diff --git a/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Validator.cs b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Validator.cs new file mode 100644 index 00000000..41310dc1 --- /dev/null +++ b/src/Microsoft.Sbom.Parsers.Spdx30SbomParser/Validator.cs @@ -0,0 +1,31 @@ +// 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.IO; +using Microsoft.Sbom.Extensions; +using Microsoft.Sbom.Extensions.Entities; +using Microsoft.Sbom.Parser; + +namespace Microsoft.Sbom.Parsers.Spdx30SbomParser; + +/// +/// Validates files in a folder against their checksums stored in an SPDX 2.2 SBOM. +/// +public class Validator : IManifestInterface +{ + public string Version { get; set; } + + private readonly ManifestInfo spdxManifestInfo = new() + { + Name = Constants.SPDXName, + Version = Constants.SPDXVersion + }; + + public ManifestData ParseManifest(string manifest) + => throw new NotImplementedException($"Currently we don't support parsing complete SPDX 2.2 SBOMs"); + + public ManifestInfo[] RegisterManifest() => new[] { spdxManifestInfo }; + + public ISbomParser CreateParser(Stream stream) => new SPDX30Parser(stream); +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomDocCreationJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomDocCreationJsonStrings.cs index 1394e2c4..a1832056 100644 --- a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomDocCreationJsonStrings.cs +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomDocCreationJsonStrings.cs @@ -42,14 +42,10 @@ public static class SbomDocCreationJsonStrings }, { ""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"" - } - ], + ""namespaceMap"": + { + ""sbom"":""http://sbom.microsoft/sbom-package-name/sbom-package-version/some-custom-value-here"" + }, ""profileConformance"": [ ""software"", ""core"", diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFileJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFileJsonStrings.cs index c05bc2a9..d311a347 100644 --- a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFileJsonStrings.cs +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFileJsonStrings.cs @@ -9,9 +9,9 @@ public static class SbomFileJsonStrings public const string FileWithLicensesAndHashes = @"[ { - ""CopyrightText"": ""sampleCopyright"", - ""creationInfo"": ""_:creationinfo"", ""name"": ""./sample/path"", + ""software_copyrightText"": ""sampleCopyright"", + ""creationInfo"": ""_:creationinfo"", ""spdxId"": ""SPDXRef-software_File-B4A9F99A3A03B9273AE34753D96564CB4F2B0FAD885BBD36B0DD619E9E8AC967"", ""verifiedUsing"": [ { diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFullDocWithFilesStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFullDocWithFilesStrings.cs new file mode 100644 index 00000000..4c017ef8 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFullDocWithFilesStrings.cs @@ -0,0 +1,459 @@ +// 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 SbomFullDocWithFilesStrings +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomWithValidFileJsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""software_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"": ""Microsoft"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-3.0.2-preview.0.41"", + ""spdxId"": ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": ""2023-05-11T00:24:54Z"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""name"": ""spdx-doc-name"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomFileWithMissingVerificationJsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""software_copyrightText"": ""sampleCopyright"", + ""creationInfo"": ""_:creationinfo"", + ""name"": ""./sample/path"", + ""spdxId"": ""SPDXRef-software_File-B4A9F99A3A03B9273AE34753D96564CB4F2B0FAD885BBD36B0DD619E9E8AC967"", + ""type"": ""software_File"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-3.0.2-preview.0.41"", + ""spdxId"": ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": ""2023-05-11T00:24:54Z"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""name"": ""spdx-doc-name"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomFileWithMissingSHA256JsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""software_copyrightText"": ""sampleCopyright"", + ""creationInfo"": ""_:creationinfo"", + ""verifiedUsing"": [ + { + ""algorithm"": ""sha1"", + ""hashValue"": ""sha1value"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-PackageVerificationCode-B1565820A5CDAC40E0520D23F9D0B1497F240DDC51D72EAC6423D97D952D444F"", + ""type"": ""PackageVerificationCode"" + } + ], + ""name"": ""./sample/path"", + ""spdxId"": ""SPDXRef-software_File-B4A9F99A3A03B9273AE34753D96564CB4F2B0FAD885BBD36B0DD619E9E8AC967"", + ""type"": ""software_File"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-3.0.2-preview.0.41"", + ""spdxId"": ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": ""2023-05-11T00:24:54Z"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""name"": ""spdx-doc-name"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomFileWithMissingNameJsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""software_copyrightText"": ""sampleCopyright"", + ""creationInfo"": ""_:creationinfo"", + ""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"": ""Microsoft"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-3.0.2-preview.0.41"", + ""spdxId"": ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": ""2023-05-11T00:24:54Z"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""name"": ""spdx-doc-name"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomFileWithMissingSpdxIdJsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""software_copyrightText"": ""sampleCopyright"", + ""creationInfo"": ""_:creationinfo"", + ""name"": ""./sample/path"", + ""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"": ""Microsoft"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-3.0.2-preview.0.41"", + ""spdxId"": ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": ""2023-05-11T00:24:54Z"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""name"": ""spdx-doc-name"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomFileWithAdditionalPropertiesJsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""software_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"", + ""additionalProperty"": ""additionalValue"", + ""additionalPropertyArray"": [""additionalValue1"", ""additionalValue2""], + ""additionalPropertyObject"": {""additionalPropertyObjectKey"": ""additionalPropertyObjectValue""}, + ""additionalPropertyWithArrayChildProperty"": [ + {""childAddtionalProperty"": ""Additional property value""} + ], + ""additionalPropertyWithObjectChildProperty"": { + ""childAddtionalProperty"": ""Additional property value"" + } + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-3.0.2-preview.0.41"", + ""spdxId"": ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": ""2023-05-11T00:24:54Z"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""name"": ""spdx-doc-name"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFullDocWithMetadataJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFullDocWithMetadataJsonStrings.cs new file mode 100644 index 00000000..fdc8807a --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomFullDocWithMetadataJsonStrings.cs @@ -0,0 +1,197 @@ +// 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 SbomFullDocWithMetadataJsonStrings +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomWithValidMetadataJsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-3.0.2-preview.0.41"", + ""spdxId"": ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": ""2023-05-11T00:24:54Z"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""CC0-1.0"", + ""spdxId"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""type"": ""AnyLicenseInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""name"": ""spdx-doc-name"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomWithSpdxDocumentMissingNameJsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft"", + ""spdxId"": ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"", + ""type"": ""Organization"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""Microsoft.SBOMTool-3.0.2-preview.0.41"", + ""spdxId"": ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"", + ""type"": ""Tool"" + }, + { + ""@id"": ""_:creationinfo"", + ""created"": ""2023-05-11T00:24:54Z"", + ""createdBy"": [ + ""SPDXRef-Organization-4B8D792FFFFCD3AF92D53A739B6DF98DF2B1F367C2745DDC0085B30F51EBBC81"" + ], + ""createdUsing"": [ + ""SPDXRef-Tool-F3816A2B734CA08686741B17A8BC9020B8513FCE6A7BD33B1006102E2A1B55AA"" + ], + ""specVersion"": ""3.0"", + ""creationInfo"": ""_:creationinfo"", + ""spdxId"": ""SPDXRef-CreationInfo-0799B4D592549CF6159C30BA3E278BF063A6A241B8728C18E7AEC18BFC2CFF6F"", + ""type"": ""CreationInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""CC0-1.0"", + ""spdxId"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""type"": ""AnyLicenseInfo"" + }, + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string SbomWithInvalidContextJsonString = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"", + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + { + ""creationInfo"": ""_:creationinfo"", + ""name"": ""the-package-namethe-package-version"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + ] + } + "; + + public const string MalformedJsonEmptyObject = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": + { + }"; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + public const string MalformedJsonEmptyArray = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": + [ + { + ""creationInfo"": ""_:creationinfo"", + ""namespaceMap"": { + ""sbom"": ""https://sbom.microsoft/1:EUb7DmXV0UyaZH1sttfV8A:n5KOcNVrWkGNryWx2sCN2A/13824:29192253/5qzhWC8k2k2wzStO28rMVQ"" + }, + ""rootElement"": [ + ""root-element-example"" + ], + ""dataLicense"": ""SPDXRef-AnyLicenseInfo-6E237C55B0583CB7BBA05562316C54B0A105ABA04775017E2253237B9A64613C"", + ""profileConformance"": [ + ""software"", + ""core"" + ], + ""name"": ""spdx-doc-name"", + ""spdxId"": ""SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1"", + ""type"": ""SpdxDocument"" + } + }"; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "JSON002:Probable JSON string detected", Justification = "Need to use JSON string")] + + public const string JsonEmptyArray = + @" + { + ""@context"": [ + ""https://spdx.org/rdf/3.0.1/spdx-context.jsonld"" + ], + ""@graph"": [ + ] + }"; +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomPackageJsonStrings.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomPackageJsonStrings.cs index 15f89746..b949a9e8 100644 --- a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomPackageJsonStrings.cs +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/JsonStrings/SbomPackageJsonStrings.cs @@ -16,13 +16,13 @@ public static class SbomPackageJsonStrings }, { ""suppliedBy"": ""SPDXRef-Organization-8560FC6692684D8DF52223FF78E30B9630A1CF5A6FA371AAE24FCA896AE20969"", - ""CopyrightText"": ""NOASSERTION"", + ""name"": ""test"", + ""software_copyrightText"": ""NOASSERTION"", ""software_downloadLocation"": ""NOASSERTION"", ""creationInfo"": ""_:creationinfo"", ""externalIdentifier"": [ ""SPDXRef-ExternalIdentifier-CE6B7E4A59503594D0AF89492494E05071399F36D9085F1E722497D78A9404E1"" ], - ""name"": ""test"", ""spdxId"": ""SPDXRef-software_Package-4739C82D88855A138C811B8CE05CC97113BEC7F7C7F66EC7E4C6C176EEA0FECE"", ""type"": ""software_Package"" }, @@ -84,14 +84,14 @@ public static class SbomPackageJsonStrings ""type"": ""Organization"" }, { - ""CopyrightText"": ""NOASSERTION"", + ""name"": ""the-package-name"", + ""software_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"": [ { diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomFileParserTests.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomFileParserTests.cs new file mode 100644 index 00000000..40d6e0ea --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomFileParserTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using System.Text; +using Microsoft.Sbom.JsonAsynchronousNodeKit.Exceptions; +using Microsoft.Sbom.Parser.JsonStrings; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Sbom.Parser; + +[TestClass] +public class SbomFileParserTests : SbomParserTestsBase +{ + [DataTestMethod] + [DataRow(SbomFullDocWithFilesStrings.SbomFileWithMissingNameJsonString)] + [DataRow(SbomFullDocWithFilesStrings.SbomFileWithMissingSpdxIdJsonString)] + [TestMethod] + public void MissingPropertiesTest_Throws(string json) + { + var bytes = Encoding.UTF8.GetBytes(json); + using var stream = new MemoryStream(bytes); + var parser = new SPDX30Parser(stream); + _ = Assert.ThrowsException(() => this.Parse(parser)); + } + + [TestMethod] + public void MissingPropertiesTest_NTIA_NoVerificationCode_Throws() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithFilesStrings.SbomFileWithMissingVerificationJsonString); + using var stream = new MemoryStream(bytes); + var parser = new SPDX30Parser(stream, requiredComplianceStandard: "NTIA"); + Assert.ThrowsException(() => this.Parse(parser)); + } + + [TestMethod] + public void MissingPropertiesTest_NTIA_VerificationCodeWithNoSHA256_Throws() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithFilesStrings.SbomFileWithMissingSHA256JsonString); + using var stream = new MemoryStream(bytes); + var parser = new SPDX30Parser(stream, requiredComplianceStandard: "NTIA"); + Assert.ThrowsException(() => this.Parse(parser)); + } + + [TestMethod] + public void IgnoresAdditionalPropertiesTest() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithFilesStrings.SbomFileWithAdditionalPropertiesJsonString); + using var stream = new MemoryStream(bytes); + var parser = new SPDX30Parser(stream); + var result = this.Parse(parser); + Assert.AreEqual(1, result.FilesCount); + } +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomMetadataParserTests.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomMetadataParserTests.cs new file mode 100644 index 00000000..2c264632 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomMetadataParserTests.cs @@ -0,0 +1,157 @@ +// 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.IO; +using System.Linq; +using System.Text; +using Microsoft.Sbom.JsonAsynchronousNodeKit.Exceptions; +using Microsoft.Sbom.Parser.JsonStrings; +using Microsoft.Sbom.Parsers.Spdx30SbomParser; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Sbom.Parser; + +[TestClass] +public class SbomMetadataParserTests : SbomParserTestsBase +{ + [TestMethod] + public void MetadataPopulates() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithMetadataJsonStrings.SbomWithValidMetadataJsonString); + using var stream = new MemoryStream(bytes); + + var parser = new SPDX30Parser(stream); + + var results = this.Parse(parser); + Assert.AreEqual(5, results.FormatEnforcedSPDX3Result.Graph.Count()); + + var metadata = parser.GetMetadata(); + Assert.IsNotNull(metadata); + Assert.IsNotNull(metadata.DocumentNamespace); + Assert.AreEqual("spdx-doc-name", metadata.Name); + Assert.AreEqual("SPDXRef-SpdxDocument-B93EED20C16A89A887B753958D42B794DD3C6570D3C2725B56B43477B38E05A1", metadata.SpdxId); + Assert.AreEqual("root-element-example", metadata.DocumentDescribes.First()); + Assert.AreEqual("CC0-1.0", metadata.DataLicense); + Assert.AreEqual(2, metadata.CreationInfo.Creators.Count()); + Assert.AreNotEqual(DateTime.MinValue, metadata.CreationInfo.Created); + Assert.AreEqual("3.0", metadata.SpdxVersion); + } + + [TestMethod] + public void MetadataEmpty() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithMetadataJsonStrings.JsonEmptyArray); + using var stream = new MemoryStream(bytes); + + var parser = new SPDX30Parser(stream); + + var results = this.Parse(parser); + + var metadata = parser.GetMetadata(); + Assert.IsNotNull(metadata); + Assert.IsNull(metadata.DocumentNamespace); + Assert.IsNull(metadata.Name); + Assert.IsNull(metadata.SpdxId); + Assert.IsNull(metadata.DocumentDescribes); + Assert.IsNull(metadata.DataLicense); + Assert.IsNull(metadata.CreationInfo); + Assert.IsNull(metadata.SpdxVersion); + } + + [TestMethod] + public void DocCreation_NoName_NTIA_Throws() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithMetadataJsonStrings.SbomWithSpdxDocumentMissingNameJsonString); + using var stream = new MemoryStream(bytes); + + var parser = new SPDX30Parser(stream, requiredComplianceStandard: "NTIA"); + Assert.ThrowsException(() => this.Parse(parser)); + } + + [TestMethod] + public void DocCreation_NoName_Passes() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithMetadataJsonStrings.SbomWithSpdxDocumentMissingNameJsonString); + using var stream = new MemoryStream(bytes); + + var parser = new SPDX30Parser(stream); + var results = this.Parse(parser); + Assert.AreEqual(5, results.FormatEnforcedSPDX3Result.Graph.Count()); + + var metadata = parser.GetMetadata(); + Assert.IsNotNull(metadata); + } + + [TestMethod] + public void InvalidContextThrows() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithMetadataJsonStrings.SbomWithInvalidContextJsonString); + using var stream = new MemoryStream(bytes); + + var parser = new SPDX30Parser(stream); + Assert.ThrowsException(() => this.Parse(parser)); + } + + [TestMethod] + public void NullStreamThrows() + { + _ = Assert.ThrowsException(() => new SPDXParser(null)); + } + + [TestMethod] + public void StreamClosedTestReturnsNull() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithMetadataJsonStrings.SbomWithValidMetadataJsonString); + using var stream = new MemoryStream(bytes); + + var parser = new SPDX30Parser(stream); + + Assert.ThrowsException(() => this.Parse(parser, stream, close: true)); + } + + [TestMethod] + public void StreamEmptyTestReturnsNull() + { + using var stream = new MemoryStream(); + stream.Read(new byte[Constants.ReadBufferSize]); + var buffer = new byte[Constants.ReadBufferSize]; + + Assert.ThrowsException(() => new SPDXParser(stream)); + } + + [DataTestMethod] + [DataRow(SbomFullDocWithMetadataJsonStrings.MalformedJsonEmptyObject)] + [DataRow(SbomFullDocWithMetadataJsonStrings.MalformedJsonEmptyArray)] + [TestMethod] + public void MalformedJsonTest_Throws(string json) + { + var bytes = Encoding.UTF8.GetBytes(json); + using var stream = new MemoryStream(bytes); + + var parser = new SPDX30Parser(stream); + Assert.ThrowsException(() => this.Parse(parser)); + } + + [TestMethod] + public void EmptyArray_ValidJson() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithMetadataJsonStrings.JsonEmptyArray); + using var stream = new MemoryStream(bytes); + + var parser = new SPDX30Parser(stream); + + var result = this.Parse(parser); + + Assert.AreEqual(0, result.FilesCount); + } + + [TestMethod] + public void NullOrEmptyBuffer_Throws() + { + var bytes = Encoding.UTF8.GetBytes(SbomFullDocWithMetadataJsonStrings.MalformedJsonEmptyObject); + using var stream = new MemoryStream(bytes); + + Assert.ThrowsException(() => new SPDX30Parser(stream, bufferSize: 0)); + } +} diff --git a/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomParserTestsBase.cs b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomParserTestsBase.cs new file mode 100644 index 00000000..bc682214 --- /dev/null +++ b/test/Microsoft.Sbom.Parsers.Spdx30SbomParser.Tests/Parser/SbomParserTestsBase.cs @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Sbom.Contracts; +using Microsoft.Sbom.JsonAsynchronousNodeKit; +using Microsoft.Sbom.JsonAsynchronousNodeKit.Exceptions; +using Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities; + +namespace Microsoft.Sbom.Parser; + +#nullable enable + +public abstract class SbomParserTestsBase +{ + public ParserResults Parse(SPDX30Parser parser, Stream? stream = null, bool close = false) + { + var results = new ParserResults(); + + ParserStateResult? result = null; + do + { + result = parser.Next(); + + if (close) + { + if (stream is not null) + { + stream.Close(); + } + else + { + throw new NotImplementedException("Can't close a stream without the stream."); + } + } + + if (result is not null && result.Result is not null) + { + var jsonElements = result.Result as IEnumerable; + var jsonList = jsonElements?.ToList(); + results.FormatEnforcedSPDX3Result ??= new FormatEnforcedSPDX3(); + switch (result.FieldName) + { + case SPDX30Parser.ContextProperty: + if (jsonList == null || !jsonList.Any() || jsonList.Count > 1) + { + throw new ParserException($"The context property is either empty or has more than one string."); + } + else + { + results.FormatEnforcedSPDX3Result.Context = (string)jsonList.First(); + } + + break; + case SPDX30Parser.GraphProperty: + results.FormatEnforcedSPDX3Result.Graph = ConvertToElements(jsonList, ref results, parser.RequiredComplianceStandard, parser.EntitiesToEnforceComplianceStandardsFor); + parser.Metadata = this.SetMetadata(results); + break; + default: + Console.WriteLine($"Unrecognized FieldName: {result.FieldName}"); + break; + } + } + } + while (result is not null); + + return results; + } + + /// + /// Converts JSON objects to SPDX elements. + /// + /// + /// + public List ConvertToElements(List? jsonList, ref ParserResults results, string? requiredComplianceStandard, IReadOnlyCollection? entitiesWithDifferentNTIARequirements) + { + var elementsList = new List(); + + if (jsonList is null) + { + return elementsList; + } + + foreach (JsonObject jsonObject in jsonList) + { + var entityType = GetEntityType(jsonObject, requiredComplianceStandard, entitiesWithDifferentNTIARequirements); + + object? deserializedElement = null; + try + { + deserializedElement = JsonSerializer.Deserialize(jsonObject.ToString(), entityType); + } + catch (Exception e) + { + throw new ParserException(e.Message); + } + + if (deserializedElement != null) + { + elementsList.Add((Element)deserializedElement); + + switch (entityType?.Name) + { + case string name when name.Contains("File"): + results.FilesCount += 1; + break; + case string name when name.Contains("Package"): + results.PackagesCount += 1; + break; + case string name when name.Contains("ExternalMap"): + results.ReferencesCount += 1; + break; + case string name when name.Contains("Relationship"): + results.RelationshipsCount += 1; + break; + default: + Console.WriteLine($"Unrecognized entity type: {entityType?.Name}"); + break; + } + } + } + + // Validate if elements meet required compliance standards + switch (requiredComplianceStandard) + { + case "NTIA": + ValidateNTIARequirements(elementsList); + break; + case null: + Console.WriteLine("Invalid or no compliance standard specified."); + break; + default: + throw new ParserException("Invalid compliance standards"); + } + + return elementsList; + } + + public Type GetEntityType(JsonObject jsonObject, string? requiredComplianceStandard, IReadOnlyCollection? entitiesWithDifferentNTIARequirements) + { + var assembly = typeof(Element).Assembly; + var entityType = jsonObject["type"]?.ToString(); + + // For these special cases, remove the prefix from the type. + switch (entityType) + { + case "software_File": + entityType = "File"; + break; + case "software_Package": + entityType = "Package"; + break; + } + + // Validate properties based on required compliance standard. + if (requiredComplianceStandard != null && entitiesWithDifferentNTIARequirements != null && entitiesWithDifferentNTIARequirements.Contains(entityType)) + { + switch (requiredComplianceStandard) + { + case "NTIA": + entityType = "NTIA" + entityType; + break; + } + } + + var type = assembly.GetType($"Microsoft.Sbom.Parsers.Spdx30SbomParser.Entities.{entityType}") ?? throw new ParserException($"{entityType} on {jsonObject} is invalid."); + + return type; + } + + public void ValidateNTIARequirements(List elementsList) + { + ValidateSbomDocCreationForNTIA(elementsList); + ValidateSbomFilesForNTIA(elementsList); + ValidateSbomPackagesForNTIA(elementsList); + } + + /// + /// Validate that information about the SBOM document is present. + /// + /// + /// + public void ValidateSbomDocCreationForNTIA(List elementsList) + { + var spdxDocumentElements = elementsList.Where(element => element is SpdxDocument); + if (spdxDocumentElements.Count() != 1) + { + throw new ParserException("SBOM document is not NTIA compliant because it must only contain one SpdxDocument element."); + } + + var spdxDocumentElement = spdxDocumentElements.First(); + + var spdxCreationInfoElement = (CreationInfo?)elementsList. + Where(element => element.Type == nameof(CreationInfo)). + FirstOrDefault(element => ((CreationInfo)element).Id == spdxDocumentElement.CreationInfoDetails) + ?? throw new ParserException($"SBOM document is not NTIA compliant because it must have a creationInfo element with ID of {spdxDocumentElement.CreationInfoDetails}"); + } + + /// + /// Validate that all files have declared and concluded licenses. + /// + /// + /// + public void ValidateSbomFilesForNTIA(List elementsList) + { + var fileElements = elementsList.Where(element => element is NTIAFile); + foreach (var fileElement in fileElements) + { + var fileSpdxId = fileElement.SpdxId; + + var fileHasSha256Hash = fileElement.VerifiedUsing. + Any(packageVerificationCode => packageVerificationCode.Algorithm == + Parsers.Spdx30SbomParser.Entities.Enums.HashAlgorithm.sha256); + + if (!fileHasSha256Hash) + { + throw new ParserException($"SBOM document is not NTIA compliant because file with SPDX ID {fileSpdxId} does not have a SHA256 hash."); + } + } + } + + /// + /// Validate that all packages have declared and concluded licenses. + /// + /// + /// + public void ValidateSbomPackagesForNTIA(List elementsList) + { + var packageElements = elementsList.Where(element => element is Package); + foreach (var packageElement in packageElements) + { + var packageSpdxId = packageElement.SpdxId; + + var packageHasSha256Hash = packageElement.VerifiedUsing. + Any(packageVerificationCode => packageVerificationCode.Algorithm == + Parsers.Spdx30SbomParser.Entities.Enums.HashAlgorithm.sha256); + + if (!packageHasSha256Hash) + { + throw new ParserException($"SBOM document is not NTIA compliant because package with SPDX ID {packageSpdxId} does not have a SHA256 hash."); + } + } + } + + /// + /// Sets metadata based on parsed SBOM elements. + /// + /// + public SpdxMetadata SetMetadata(ParserResults result) + { + var metadata = new SpdxMetadata(); + var spdxDocumentElement = (SpdxDocument?)result.FormatEnforcedSPDX3Result.Graph.FirstOrDefault(element => element.Type == "SpdxDocument"); + + if (spdxDocumentElement == null) + { + return metadata; + } + + if (spdxDocumentElement.NamespaceMap != null && spdxDocumentElement.NamespaceMap.TryGetValue("sbom", out var namespaceUri)) + { + metadata.DocumentNamespace = new Uri(namespaceUri); + } + + metadata.Name = spdxDocumentElement.Name; + metadata.SpdxId = spdxDocumentElement.SpdxId; + metadata.DocumentDescribes = spdxDocumentElement.RootElement; + + var dataLicenseSpdxId = spdxDocumentElement.DataLicense; + var spdxDataLicenseElement = result.FormatEnforcedSPDX3Result.Graph. + FirstOrDefault(element => element.SpdxId == dataLicenseSpdxId) as AnyLicenseInfo; + metadata.DataLicense = spdxDataLicenseElement?.Name; + + var spdxCreationInfoElement = (CreationInfo?)result.FormatEnforcedSPDX3Result.Graph. + Where(element => element.Type == nameof(CreationInfo)). + FirstOrDefault(element => ((CreationInfo)element).Id == spdxDocumentElement.CreationInfoDetails); + + var creators = spdxCreationInfoElement?.CreatedBy + .Select(createdBy => result.FormatEnforcedSPDX3Result.Graph. + FirstOrDefault(element => element.SpdxId == createdBy) as Organization) + .Where(spdxOrganizationElement => spdxOrganizationElement != null) + .Select(spdxOrganizationElement => spdxOrganizationElement?.Name) + .ToList() ?? []; + + creators.AddRange(spdxCreationInfoElement?.CreatedUsing + .Select(createdBy => result.FormatEnforcedSPDX3Result.Graph. + FirstOrDefault(element => element.SpdxId == createdBy) as Tool) + .Where(spdxToolElement => spdxToolElement != null) + .Select(spdxToolElement => spdxToolElement?.Name) ?? []); + + var createdDate = DateTime.MinValue; + DateTime.TryParse(spdxCreationInfoElement?.Created, out createdDate); + + metadata.CreationInfo = new MetadataCreationInfo + { + Created = createdDate, + Creators = creators, + }; + + metadata.SpdxVersion = spdxCreationInfoElement?.SpecVersion; + + return metadata; + } +}