From 5f10aea00cf46bbe3a4ce66ce2b85bd17576a35c Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 26 Sep 2022 19:37:51 +0200 Subject: [PATCH] refactor: refine spec version conversion to cover more cases improves on #51 Signed-off-by: nscuro --- convert.go | 304 +++++++++++++++++++++++++++++++++++++++++++++++++++ cyclonedx.go | 1 + downgrade.go | 166 ---------------------------- encode.go | 8 +- 4 files changed, 309 insertions(+), 170 deletions(-) create mode 100644 convert.go delete mode 100644 downgrade.go diff --git a/convert.go b/convert.go new file mode 100644 index 0000000..f0da0c8 --- /dev/null +++ b/convert.go @@ -0,0 +1,304 @@ +// This file is part of CycloneDX Go +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +package cyclonedx + +import "fmt" + +// copyAndConvert returns a converted copy of the BOM, adhering to a given SpecVersion. +func (b BOM) copyAndConvert(specVersion SpecVersion) (*BOM, error) { + var bomCopy BOM + err := b.copy(&bomCopy) + if err != nil { + return nil, fmt.Errorf("failed to copy bom: %w", err) + } + + bomCopy.convert(specVersion) + return &bomCopy, nil +} + +// convert modifies the BOM such that it adheres to a given SpecVersion. +func (b *BOM) convert(specVersion SpecVersion) { + if specVersion < SpecVersion1_1 { + b.SerialNumber = "" + b.ExternalReferences = nil + } + if specVersion < SpecVersion1_2 { + b.Dependencies = nil + b.Metadata = nil + b.Services = nil + } + if specVersion < SpecVersion1_3 { + b.Compositions = nil + } + if specVersion < SpecVersion1_4 { + b.Vulnerabilities = nil + } + + if b.Metadata != nil { + if specVersion < SpecVersion1_3 { + b.Metadata.Licenses = nil + b.Metadata.Properties = nil + } + + recurseComponent(b.Metadata.Component, componentConverter(specVersion)) + convertLicenses(b.Metadata.Licenses, specVersion) + if b.Metadata.Tools != nil { + for i := range *b.Metadata.Tools { + convertTool(&(*b.Metadata.Tools)[i], specVersion) + } + } + } + + if b.Components != nil { + for i := range *b.Components { + recurseComponent(&(*b.Components)[i], componentConverter(specVersion)) + } + } + + if b.Services != nil { + for i := range *b.Services { + recurseService(&(*b.Services)[i], serviceConverter(specVersion)) + } + } + + b.SpecVersion = specVersion + b.XMLNS = xmlNamespaces[specVersion] +} + +// componentConverter modifies a Component such that it adheres to a given SpecVersion. +func componentConverter(specVersion SpecVersion) func(*Component) { + return func(c *Component) { + if specVersion < SpecVersion1_1 { + c.BOMRef = "" + c.ExternalReferences = nil + if c.Modified == nil { + c.Modified = Bool(false) + } + c.Pedigree = nil + } + + if specVersion < SpecVersion1_2 { + c.Author = "" + c.MIMEType = "" + if c.Pedigree != nil { + c.Pedigree.Patches = nil + } + c.Supplier = nil + c.SWID = nil + } + + if specVersion < SpecVersion1_3 { + c.Evidence = nil + c.Properties = nil + } + + if specVersion < SpecVersion1_4 { + c.ReleaseNotes = nil + if c.Version == "" { + c.Version = "0.0.0" + } + } + + if !specVersion.supportsComponentType(c.Type) { + c.Type = ComponentTypeApplication + } + convertExternalReferences(c.ExternalReferences, specVersion) + convertHashes(c.Hashes, specVersion) + convertLicenses(c.Licenses, specVersion) + if !specVersion.supportsScope(c.Scope) { + c.Scope = "" + } + } +} + +// convertExternalReferences modifies an ExternalReference slice such that it adheres to a given SpecVersion. +func convertExternalReferences(extRefs *[]ExternalReference, specVersion SpecVersion) { + if extRefs == nil { + return + } + + if specVersion < SpecVersion1_3 { + for i := range *extRefs { + (*extRefs)[i].Hashes = nil + } + } +} + +// convertHashes modifies a Hash slice such that it adheres to a given SpecVersion. +// If after the conversion no valid hashes are left in the slice, it will be nilled. +func convertHashes(hashes *[]Hash, specVersion SpecVersion) { + if hashes == nil { + return + } + + converted := make([]Hash, 0) + for i := range *hashes { + hash := (*hashes)[i] + if specVersion.supportsHashAlgorithm(hash.Algorithm) { + converted = append(converted, hash) + } + } + + if len(converted) == 0 { + *hashes = nil + } else { + *hashes = converted + } +} + +// convertLicenses modifies a Licenses slice such that it adheres to a given SpecVersion. +// If after the conversion no valid licenses are left in the slice, it will be nilled. +func convertLicenses(licenses *Licenses, specVersion SpecVersion) { + if licenses == nil { + return + } + + if specVersion < SpecVersion1_1 { + converted := make(Licenses, 0) + for i := range *licenses { + choice := &(*licenses)[i] + if choice.License != nil { + if choice.License.ID == "" && choice.License.Name == "" { + choice.License = nil + } else { + choice.License.Text = nil + choice.License.URL = "" + } + } + choice.Expression = "" + if choice.License != nil { + converted = append(converted, *choice) + } + } + + if len(converted) == 0 { + *licenses = nil + } else { + *licenses = converted + } + } +} + +// serviceConverter modifies a Service such that it adheres to a given SpecVersion. +func serviceConverter(specVersion SpecVersion) func(*Service) { + return func(s *Service) { + if specVersion < SpecVersion1_3 { + s.Properties = nil + } + + if specVersion < SpecVersion1_4 { + s.ReleaseNotes = nil + } + + convertExternalReferences(s.ExternalReferences, specVersion) + } +} + +// convertTool modifies a Tool such that it adheres to a given SpecVersion. +func convertTool(tool *Tool, specVersion SpecVersion) { + if tool == nil { + return + } + + if specVersion < SpecVersion1_4 { + tool.ExternalReferences = nil + } + + convertExternalReferences(tool.ExternalReferences, specVersion) + convertHashes(tool.Hashes, specVersion) +} + +func recurseComponent(component *Component, f func(c *Component)) { + if component == nil { + return + } + + f(component) + + if component.Components != nil { + for i := range *component.Components { + recurseComponent(&(*component.Components)[i], f) + } + } + if component.Pedigree != nil { + if component.Pedigree.Ancestors != nil { + for i := range *component.Pedigree.Ancestors { + recurseComponent(&(*component.Pedigree.Ancestors)[i], f) + } + } + if component.Pedigree.Descendants != nil { + for i := range *component.Pedigree.Descendants { + recurseComponent(&(*component.Pedigree.Descendants)[i], f) + } + } + if component.Pedigree.Variants != nil { + for i := range *component.Pedigree.Variants { + recurseComponent(&(*component.Pedigree.Variants)[i], f) + } + } + } +} + +func recurseService(service *Service, f func(s *Service)) { + if service == nil { + return + } + + f(service) + + if service.Services != nil { + for i := range *service.Services { + recurseService(&(*service.Services)[i], f) + } + } +} + +func (sv SpecVersion) supportsComponentType(cType ComponentType) bool { + switch cType { + case ComponentTypeApplication, ComponentTypeDevice, ComponentTypeFramework, ComponentTypeLibrary, ComponentTypeOS: + return sv >= SpecVersion1_0 + case ComponentTypeFile: + return sv >= SpecVersion1_1 + case ComponentTypeContainer, ComponentTypeFirmware: + return sv >= SpecVersion1_2 + } + + return false +} + +func (sv SpecVersion) supportsHashAlgorithm(algo HashAlgorithm) bool { + switch algo { + case HashAlgoMD5, HashAlgoSHA1, HashAlgoSHA256, HashAlgoSHA384, HashAlgoSHA512, HashAlgoSHA3_256, HashAlgoSHA3_512: + return sv >= SpecVersion1_0 + case HashAlgoSHA3_384, HashAlgoBlake2b_256, HashAlgoBlake2b_384, HashAlgoBlake2b_512, HashAlgoBlake3: + return sv >= SpecVersion1_2 + } + + return false +} + +func (sv SpecVersion) supportsScope(scope Scope) bool { + switch scope { + case ScopeRequired, ScopeOptional: + return sv >= SpecVersion1_0 + case ScopeExcluded: + return sv >= SpecVersion1_2 + } + + return false +} diff --git a/cyclonedx.go b/cyclonedx.go index 69c2b18..1ac57f7 100644 --- a/cyclonedx.go +++ b/cyclonedx.go @@ -239,6 +239,7 @@ const ( HashAlgoSHA384 HashAlgorithm = "SHA-384" HashAlgoSHA512 HashAlgorithm = "SHA-512" HashAlgoSHA3_256 HashAlgorithm = "SHA3-256" + HashAlgoSHA3_384 HashAlgorithm = "SHA3-384" HashAlgoSHA3_512 HashAlgorithm = "SHA3-512" HashAlgoBlake2b_256 HashAlgorithm = "BLAKE2b-256" HashAlgoBlake2b_384 HashAlgorithm = "BLAKE2b-384" diff --git a/downgrade.go b/downgrade.go deleted file mode 100644 index 5523be2..0000000 --- a/downgrade.go +++ /dev/null @@ -1,166 +0,0 @@ -// This file is part of CycloneDX Go -// -// Licensed under the Apache License, Version 2.0 (the “License”); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an “AS IS” BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 -// Copyright (c) OWASP Foundation. All Rights Reserved. - -package cyclonedx - -import "fmt" - -// downgrade "downgrades" the BOM to a given version of the specification. -// Downgrading works by successively removing (or changing) fields introduced in later specification versions. -// This procedure has been adapted from the .NET implementation: -// https://github.com/CycloneDX/cyclonedx-dotnet-library/blob/v5.2.2/src/CycloneDX.Core/BomUtils.cs#L60 -func (b *BOM) downgrade(version SpecVersion) error { - if version < SpecVersion1_1 { - b.SerialNumber = "" - b.ExternalReferences = nil - forEachComponent(b.Components, func(c *Component) { - c.BOMRef = "" - c.ExternalReferences = nil - if c.Licenses != nil { - // Keep track of licenses that are still valid - // after removal of unsupported fields. - validLicenses := make(Licenses, 0) - - for i := range *c.Licenses { - license := &(*c.Licenses)[i] - if license.License != nil { - license.License.Text = nil - license.License.URL = "" - } - license.Expression = "" - if license.License != nil { - validLicenses = append(validLicenses, *license) - } - } - - // Remove the licenses node entirely if no valid licenses - // are left. This avoids empty (thus invalid) tags in XML. - if len(validLicenses) == 0 { - c.Licenses = nil - } else { - c.Licenses = &validLicenses - } - } - if c.Modified == nil { - c.Modified = Bool(false) - } - c.Pedigree = nil - }) - } - - if version < SpecVersion1_2 { - b.Metadata = nil - b.Dependencies = nil - b.Services = nil - forEachComponent(b.Components, func(c *Component) { - c.Author = "" - c.MIMEType = "" - c.Supplier = nil - c.SWID = nil - if c.Pedigree != nil { - c.Pedigree.Patches = nil - } - }) - } - - if version < SpecVersion1_3 { - b.Compositions = nil - if b.Metadata != nil { - b.Metadata.Licenses = nil - b.Metadata.Properties = nil - } - forEachComponent(b.Components, func(c *Component) { - c.Evidence = nil - c.Properties = nil - if c.ExternalReferences != nil { - for i := range *c.ExternalReferences { - (*c.ExternalReferences)[i].Hashes = nil - } - } - }) - forEachService(b.Services, func(s *Service) { - s.Properties = nil - if s.ExternalReferences != nil { - for i := range *s.ExternalReferences { - (*s.ExternalReferences)[i].Hashes = nil - } - } - }) - } - - if version < SpecVersion1_4 { - if b.Metadata != nil && b.Metadata.Tools != nil { - for i := range *b.Metadata.Tools { - (*b.Metadata.Tools)[i].ExternalReferences = nil - } - } - forEachComponent(b.Components, func(c *Component) { - c.ReleaseNotes = nil - if c.Version == "" { - c.Version = "0.0.0" - } - }) - forEachService(b.Services, func(s *Service) { - s.ReleaseNotes = nil - }) - b.Vulnerabilities = nil - } - - b.SpecVersion = version - b.XMLNS = xmlNamespaces[version] - - return nil -} - -func (b *BOM) copyAndDowngrade(version SpecVersion) (*BOM, error) { - var bomCopy BOM - err := b.copy(&bomCopy) - if err != nil { - return nil, fmt.Errorf("failed to copy bom: %w", err) - } - - err = bomCopy.downgrade(version) - return &bomCopy, err -} - -func forEachComponent(components *[]Component, f func(c *Component)) { - if components == nil || len(*components) == 0 { - return - } - - for i := range *components { - component := &(*components)[i] - f(component) - forEachComponent(component.Components, f) - if component.Pedigree != nil { - forEachComponent(component.Pedigree.Ancestors, f) - forEachComponent(component.Pedigree.Descendants, f) - forEachComponent(component.Pedigree.Variants, f) - } - } -} - -func forEachService(services *[]Service, f func(s *Service)) { - if services == nil || len(*services) == 0 { - return - } - - for i := range *services { - f(&(*services)[i]) - forEachService((*services)[i].Services, f) - } -} diff --git a/encode.go b/encode.go index 576dfcc..b03843d 100644 --- a/encode.go +++ b/encode.go @@ -64,8 +64,8 @@ func (j jsonBOMEncoder) Encode(bom *BOM) error { } // EncodeVersion implements the BOMEncoder interface. -func (j jsonBOMEncoder) EncodeVersion(bom *BOM, version SpecVersion) (err error) { - bom, err = bom.copyAndDowngrade(version) +func (j jsonBOMEncoder) EncodeVersion(bom *BOM, specVersion SpecVersion) (err error) { + bom, err = bom.copyAndConvert(specVersion) if err != nil { return } @@ -99,8 +99,8 @@ func (x xmlBOMEncoder) Encode(bom *BOM) error { } // EncodeVersion implements the BOMEncoder interface. -func (x xmlBOMEncoder) EncodeVersion(bom *BOM, version SpecVersion) (err error) { - bom, err = bom.copyAndDowngrade(version) +func (x xmlBOMEncoder) EncodeVersion(bom *BOM, specVersion SpecVersion) (err error) { + bom, err = bom.copyAndConvert(specVersion) if err != nil { return }