diff --git a/convert.go b/convert.go index d428d01..d53814b 100644 --- a/convert.go +++ b/convert.go @@ -57,6 +57,12 @@ func (b *BOM) convert(specVersion SpecVersion) { b.Definitions = nil } + if b.Dependencies != nil && specVersion < SpecVersion1_6 { + for i := range *b.Dependencies { + (*b.Dependencies)[i].Provides = nil + } + } + if b.Metadata != nil { if specVersion < SpecVersion1_3 { b.Metadata.Licenses = nil @@ -456,6 +462,10 @@ func serviceConverter(specVersion SpecVersion) func(*Service) { s.ReleaseNotes = nil } + if specVersion < SpecVersion1_5 { + s.TrustZone = "" + } + convertOrganizationalEntity(s.Provider, specVersion) convertExternalReferences(s.ExternalReferences, specVersion) } diff --git a/convert_test.go b/convert_test.go index eb47a9a..6636fb0 100644 --- a/convert_test.go +++ b/convert_test.go @@ -246,6 +246,34 @@ func Test_convertAuthors(t *testing.T) { }) } +func Test_convertTrustZone(t *testing.T) { + t.Run("spec 1.4 and lower", func(t *testing.T) { + bom := NewBOM() + bom.Services = &[]Service{ + { + Name: "Payment API", + TrustZone: "trusted", + }, + } + + bom.convert(SpecVersion1_4) + + assert.Empty(t, (*bom.Services)[0].TrustZone) + }) + + t.Run("spec 1.5 and higher", func(t *testing.T) { + bom := NewBOM() + bom.Services = &[]Service{ + { + Name: "Payment API", + TrustZone: "trusted", + }, + } + bom.convert(SpecVersion1_5) + assert.Equal(t, "trusted", (*bom.Services)[0].TrustZone) + }) +} + func Test_convertTags(t *testing.T) { t.Run("spec 1.5 and lower", func(t *testing.T) { bom := NewBOM() diff --git a/cyclonedx.go b/cyclonedx.go index 3be6b63..e421335 100644 --- a/cyclonedx.go +++ b/cyclonedx.go @@ -603,6 +603,7 @@ type Signatory struct { type Dependency struct { Ref string `json:"ref"` Dependencies *[]string `json:"dependsOn,omitempty"` + Provides *[]string `json:"provides,omitempty"` } type Diff struct { @@ -711,6 +712,8 @@ const ( ERTypeDistributionIntake ExternalReferenceType = "distribution-intake" ERTypeDocumentation ExternalReferenceType = "documentation" ERTypeDynamicAnalysisReport ExternalReferenceType = "dynamic-analysis-report" + ERTypeDigitalSignature ExternalReferenceType = "digital-signature" + ERTypeElectronicSignature ExternalReferenceType = "electronic-signature" ERTypeEvidence ExternalReferenceType = "evidence" ERTypeExploitabilityStatement ExternalReferenceType = "exploitability-statement" ERTypeFormulation ExternalReferenceType = "formulation" @@ -722,12 +725,15 @@ const ( ERTypeModelCard ExternalReferenceType = "model-card" ERTypeOther ExternalReferenceType = "other" ERTypePentestReport ExternalReferenceType = "pentest-report" + ERTypePOAM ExternalReferenceType = "poam" ERTypeQualityMetrics ExternalReferenceType = "quality-metrics" ERTypeReleaseNotes ExternalReferenceType = "release-notes" ERTypeRiskAssessment ExternalReferenceType = "risk-assessment" + ERTypeRFC9116 ExternalReferenceType = "rfc-9116" ERTypeRuntimeAnalysisReport ExternalReferenceType = "runtime-analysis-report" ERTypeSecurityContact ExternalReferenceType = "security-contact" ERTypeSocial ExternalReferenceType = "social" + ERTypeSourceDistribution ExternalReferenceType = "source-distribution" ERTypeStaticAnalysisReport ExternalReferenceType = "static-analysis-report" ERTypeSupport ExternalReferenceType = "support" ERTypeThreatModel ExternalReferenceType = "threat-model" @@ -1297,6 +1303,7 @@ type Service struct { Services *[]Service `json:"services,omitempty" xml:"services>service,omitempty"` ReleaseNotes *ReleaseNotes `json:"releaseNotes,omitempty" xml:"releaseNotes,omitempty"` Tags *[]string `json:"tags,omitempty" xml:"tags>tag,omitempty"` + TrustZone string `json:"trustZone,omitempty" xml:"trustZone,omitempty"` Signature *JSFSignature `json:"signature,omitempty" xml:"-"` } diff --git a/cyclonedx_json_test.go b/cyclonedx_json_test.go index bf1b627..c93b54b 100644 --- a/cyclonedx_json_test.go +++ b/cyclonedx_json_test.go @@ -330,3 +330,190 @@ func TestEvidence_UnmarshalJSON(t *testing.T) { }, evidence.Identity) }) } + +func TestService_TrustZone_MarshalJSON(t *testing.T) { + t.Run("WithTrustZone", func(t *testing.T) { + service := Service{ + Name: "Payment API", + TrustZone: "trusted", + } + jsonBytes, err := json.Marshal(service) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"trustZone":"trusted"`) + require.Contains(t, string(jsonBytes), `"name":"Payment API"`) + }) + + t.Run("WithoutTrustZone", func(t *testing.T) { + service := Service{ + Name: "Payment API", + } + jsonBytes, err := json.Marshal(service) + require.NoError(t, err) + require.NotContains(t, string(jsonBytes), "trustZone") + require.Contains(t, string(jsonBytes), `"name":"Payment API"`) + }) +} + +func TestService_TrustZone_UnmarshalJSON(t *testing.T) { + t.Run("WithTrustZone", func(t *testing.T) { + var service Service + err := json.Unmarshal([]byte(`{"name":"Payment API","trustZone":"trusted"}`), &service) + require.NoError(t, err) + require.Equal(t, "Payment API", service.Name) + require.Equal(t, "trusted", service.TrustZone) + }) + + t.Run("WithoutTrustZone", func(t *testing.T) { + var service Service + err := json.Unmarshal([]byte(`{"name":"Payment API"}`), &service) + require.NoError(t, err) + require.Equal(t, "Payment API", service.Name) + require.Empty(t, service.TrustZone) + }) +} + +func TestDependency_Provides_MarshalJSON(t *testing.T) { + t.Run("WithProvides", func(t *testing.T) { + dependency := Dependency{ + Ref: "crypto-library", + Provides: &[]string{"aes128gcm", "sha256"}, + } + jsonBytes, err := json.Marshal(dependency) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"ref":"crypto-library"`) + require.Contains(t, string(jsonBytes), `"provides":["aes128gcm","sha256"]`) + }) + + t.Run("WithProvidesAndDependsOn", func(t *testing.T) { + dependency := Dependency{ + Ref: "crypto-library", + Dependencies: &[]string{"base-library"}, + Provides: &[]string{"aes128gcm"}, + } + jsonBytes, err := json.Marshal(dependency) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"ref":"crypto-library"`) + require.Contains(t, string(jsonBytes), `"dependsOn":["base-library"]`) + require.Contains(t, string(jsonBytes), `"provides":["aes128gcm"]`) + }) + + t.Run("WithoutProvides", func(t *testing.T) { + dependency := Dependency{ + Ref: "app-component", + Dependencies: &[]string{"library-a"}, + } + jsonBytes, err := json.Marshal(dependency) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"ref":"app-component"`) + require.NotContains(t, string(jsonBytes), "provides") + }) +} + +func TestDependency_Provides_UnmarshalJSON(t *testing.T) { + t.Run("WithProvides", func(t *testing.T) { + var dependency Dependency + err := json.Unmarshal([]byte(`{"ref":"crypto-library","provides":["aes128gcm","sha256"]}`), &dependency) + require.NoError(t, err) + require.Equal(t, "crypto-library", dependency.Ref) + require.NotNil(t, dependency.Provides) + require.Equal(t, 2, len(*dependency.Provides)) + require.Equal(t, "aes128gcm", (*dependency.Provides)[0]) + require.Equal(t, "sha256", (*dependency.Provides)[1]) + }) + + t.Run("WithProvidesAndDependsOn", func(t *testing.T) { + var dependency Dependency + err := json.Unmarshal([]byte(`{"ref":"crypto-library","dependsOn":["base-library"],"provides":["aes128gcm"]}`), &dependency) + require.NoError(t, err) + require.Equal(t, "crypto-library", dependency.Ref) + require.NotNil(t, dependency.Dependencies) + require.Equal(t, 1, len(*dependency.Dependencies)) + require.Equal(t, "base-library", (*dependency.Dependencies)[0]) + require.NotNil(t, dependency.Provides) + require.Equal(t, 1, len(*dependency.Provides)) + require.Equal(t, "aes128gcm", (*dependency.Provides)[0]) + }) + + t.Run("WithoutProvides", func(t *testing.T) { + var dependency Dependency + err := json.Unmarshal([]byte(`{"ref":"app-component","dependsOn":["library-a"]}`), &dependency) + require.NoError(t, err) + require.Equal(t, "app-component", dependency.Ref) + require.Nil(t, dependency.Provides) + }) +} + +func TestExternalReferenceType_NewValues(t *testing.T) { + t.Run("DigitalSignature", func(t *testing.T) { + extRef := ExternalReference{ + Type: ERTypeDigitalSignature, + URL: "https://example.com/signature", + } + jsonBytes, err := json.Marshal(extRef) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"type":"digital-signature"`) + }) + + t.Run("ElectronicSignature", func(t *testing.T) { + extRef := ExternalReference{ + Type: ERTypeElectronicSignature, + URL: "https://example.com/esignature", + } + jsonBytes, err := json.Marshal(extRef) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"type":"electronic-signature"`) + }) + + t.Run("POAM", func(t *testing.T) { + extRef := ExternalReference{ + Type: ERTypePOAM, + URL: "https://example.com/poam", + } + jsonBytes, err := json.Marshal(extRef) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"type":"poam"`) + }) + + t.Run("RFC9116", func(t *testing.T) { + extRef := ExternalReference{ + Type: ERTypeRFC9116, + URL: "https://example.com/security.txt", + } + jsonBytes, err := json.Marshal(extRef) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"type":"rfc-9116"`) + }) + + t.Run("SourceDistribution", func(t *testing.T) { + extRef := ExternalReference{ + Type: ERTypeSourceDistribution, + URL: "https://example.com/source.tar.gz", + } + jsonBytes, err := json.Marshal(extRef) + require.NoError(t, err) + require.Contains(t, string(jsonBytes), `"type":"source-distribution"`) + }) + + t.Run("UnmarshalNewTypes", func(t *testing.T) { + testCases := []struct { + name string + json string + expected ExternalReferenceType + }{ + {"digital-signature", `{"type":"digital-signature","url":"https://example.com"}`, ERTypeDigitalSignature}, + {"electronic-signature", `{"type":"electronic-signature","url":"https://example.com"}`, ERTypeElectronicSignature}, + {"poam", `{"type":"poam","url":"https://example.com"}`, ERTypePOAM}, + {"rfc-9116", `{"type":"rfc-9116","url":"https://example.com"}`, ERTypeRFC9116}, + {"source-distribution", `{"type":"source-distribution","url":"https://example.com"}`, ERTypeSourceDistribution}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var extRef ExternalReference + err := json.Unmarshal([]byte(tc.json), &extRef) + require.NoError(t, err) + require.Equal(t, tc.expected, extRef.Type) + }) + } + }) +} diff --git a/cyclonedx_xml.go b/cyclonedx_xml.go index 0bb560a..6a7cb57 100644 --- a/cyclonedx_xml.go +++ b/cyclonedx_xml.go @@ -61,6 +61,7 @@ func (c *Copyright) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { type dependencyXML struct { Ref string `xml:"ref,attr"` Dependencies *[]dependencyXML `xml:"dependency,omitempty"` + Provides *[]dependencyXML `xml:"provides,omitempty"` } func (d Dependency) MarshalXML(e *xml.Encoder, start xml.StartElement) error { @@ -74,6 +75,14 @@ func (d Dependency) MarshalXML(e *xml.Encoder, start xml.StartElement) error { xmlDep.Dependencies = &xmlDeps } + if d.Provides != nil && len(*d.Provides) > 0 { + xmlProvides := make([]dependencyXML, len(*d.Provides)) + for i := range *d.Provides { + xmlProvides[i] = dependencyXML{Ref: (*d.Provides)[i]} + } + xmlDep.Provides = &xmlProvides + } + return e.EncodeElement(xmlDep, start) } @@ -93,6 +102,14 @@ func (d *Dependency) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) erro dep.Dependencies = &deps } + if xmlDep.Provides != nil && len(*xmlDep.Provides) > 0 { + provides := make([]string, len(*xmlDep.Provides)) + for i := range *xmlDep.Provides { + provides[i] = (*xmlDep.Provides)[i].Ref + } + dep.Provides = &provides + } + *d = dep return nil } diff --git a/cyclonedx_xml_test.go b/cyclonedx_xml_test.go index c158e60..9c858ee 100644 --- a/cyclonedx_xml_test.go +++ b/cyclonedx_xml_test.go @@ -707,7 +707,7 @@ func TestEvidence_UnmarshalXML(t *testing.T) { }) } -func toPointer[V int | float32](t *testing.T, v V) *V { +func toPointer[T any](t *testing.T, value T) *T { t.Helper() - return &v + return &value } diff --git a/testdata/snapshots/cyclonedx-go-TestRoundTripJSON-func1-valid-cryptographic-asset.json b/testdata/snapshots/cyclonedx-go-TestRoundTripJSON-func1-valid-cryptographic-asset.json new file mode 100644 index 0000000..84a9b2f --- /dev/null +++ b/testdata/snapshots/cyclonedx-go-TestRoundTripJSON-func1-valid-cryptographic-asset.json @@ -0,0 +1,73 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "component": { + "bom-ref": "acme-application", + "type": "application", + "name": "Acme Application", + "version": "1.0" + } + }, + "components": [ + { + "bom-ref": "aes128gcm", + "type": "cryptographic-asset", + "name": "AES", + "cryptoProperties": { + "assetType": "algorithm", + "algorithmProperties": { + "primitive": "ae", + "parameterSetIdentifier": "128", + "executionEnvironment": "software-plain-ram", + "implementationPlatform": "x86_64", + "certificationLevel": [ + "none" + ], + "mode": "gcm", + "cryptoFunctions": [ + "keygen", + "encrypt", + "decrypt", + "tag" + ], + "classicalSecurityLevel": 128, + "nistQuantumSecurityLevel": 1 + }, + "oid": "2.16.840.1.101.3.4.1.6" + } + }, + { + "bom-ref": "crypto-library", + "type": "library", + "name": "Crypto library", + "version": "1.0.0" + }, + { + "bom-ref": "some-library", + "type": "library", + "name": "Some library", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "acme-application", + "dependsOn": [ + "crypto-library" + ] + }, + { + "ref": "crypto-library", + "dependsOn": [ + "some-library" + ], + "provides": [ + "aes128gcm" + ] + } + ] +} + diff --git a/testdata/snapshots/cyclonedx-go-TestRoundTripXML-func1-valid-cryptographic-asset.xml b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-func1-valid-cryptographic-asset.xml new file mode 100644 index 0000000..0bd9d55 --- /dev/null +++ b/testdata/snapshots/cyclonedx-go-TestRoundTripXML-func1-valid-cryptographic-asset.xml @@ -0,0 +1,51 @@ + + + + + Acme Application + 1.0 + + + + + AES + + algorithm + + ae + 128 + software-plain-ram + x86_64 + none + gcm + + keygen + encrypt + decrypt + tag + + 128 + 1 + + 2.16.840.1.101.3.4.1.6 + + + + Crypto library + 1.0.0 + + + Some library + 1.0.0 + + + + + + + + + + + + diff --git a/testdata/valid-cryptographic-asset.json b/testdata/valid-cryptographic-asset.json new file mode 100644 index 0000000..e508b2c --- /dev/null +++ b/testdata/valid-cryptographic-asset.json @@ -0,0 +1,59 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "component": { + "type": "application", + "bom-ref": "acme-application", + "name": "Acme Application", + "version": "1.0" + } + }, + "components": [ + { + "type": "cryptographic-asset", + "bom-ref": "aes128gcm", + "name": "AES", + "cryptoProperties": { + "assetType": "algorithm", + "algorithmProperties": { + "primitive": "ae", + "parameterSetIdentifier": "128", + "executionEnvironment": "software-plain-ram", + "implementationPlatform": "x86_64", + "certificationLevel": [ "none" ], + "mode": "gcm", + "cryptoFunctions": ["keygen", "encrypt", "decrypt", "tag"], + "classicalSecurityLevel": 128, + "nistQuantumSecurityLevel": 1 + }, + "oid": "2.16.840.1.101.3.4.1.6" + } + }, + { + "type": "library", + "bom-ref": "crypto-library", + "name": "Crypto library", + "version": "1.0.0" + }, + { + "type": "library", + "bom-ref": "some-library", + "name": "Some library", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "acme-application", + "dependsOn": ["crypto-library"] + }, + { + "ref": "crypto-library", + "provides": ["aes128gcm"], + "dependsOn": ["some-library"] + } + ] +} \ No newline at end of file diff --git a/testdata/valid-cryptographic-asset.xml b/testdata/valid-cryptographic-asset.xml new file mode 100644 index 0000000..364efe8 --- /dev/null +++ b/testdata/valid-cryptographic-asset.xml @@ -0,0 +1,51 @@ + + + + + Acme Application + 1.0 + + + + + AES + + algorithm + + ae + 128 + software-plain-ram + x86_64 + none + gcm + + keygen + encrypt + decrypt + tag + + 128 + 1 + + 2.16.840.1.101.3.4.1.6 + + + + Crypto library + 1.0.0 + + + Some library + 1.0.0 + + + + + + + + + + + +