Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for sub-components (Models.Component.components) #136

Merged
merged 11 commits into from
Jul 29, 2022
12 changes: 8 additions & 4 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ All notable changes to this project will be documented in this file.
## unreleased

* Added
* CycloneDX spec version 1.4 made element `bom.component.version` optional.
Therefore, serialization/normalization with this spec version will no longer render this element,
when its value is empty. (via [#137], [#138])

* Support for nested/bundled (sub-)components via `Models.Component.components` was added, including
serialization/normalization of models and impact on dependency graphs rendering. ([#132] via [#136])
* CycloneDX spec version 1.4 made element `Models.Component.version` optional.
Therefore, serialization/normalization with this spec version will no longer render this element
if its value is empty. (via [#137], [#138])

[#132]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/132
[#136]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/136
[#137]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/137
[#138]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/138

Expand Down
20 changes: 20 additions & 0 deletions src/helpers/tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*!
This file is part of CycloneDX JavaScript Library.

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.
*/

export const treeIterator = Symbol('iterator of a tree/nesting-like structure')
10 changes: 10 additions & 0 deletions src/models/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ExternalReferenceRepository } from './externalReference'
import { LicenseRepository } from './license'
import { SWID } from './swid'
import { Comparable, SortableSet } from '../helpers/sortableSet'
import { treeIterator } from '../helpers/tree'

interface OptionalProperties {
bomRef?: BomRef['value']
Expand All @@ -45,6 +46,7 @@ interface OptionalProperties {
swid?: Component['swid']
version?: Component['version']
dependencies?: Component['dependencies']
components?: Component['components']
cpe?: Component['cpe']
}

Expand All @@ -65,6 +67,7 @@ export class Component implements Comparable {
swid?: SWID
version?: string
dependencies: BomRefRepository
components: ComponentRepository

/** @see bomRef */
readonly #bomRef: BomRef
Expand Down Expand Up @@ -93,6 +96,7 @@ export class Component implements Comparable {
this.version = op.version
this.description = op.description
this.dependencies = op.dependencies ?? new BomRefRepository()
this.components = op.components ?? new ComponentRepository()
this.cpe = op.cpe
}

Expand Down Expand Up @@ -134,4 +138,10 @@ export class Component implements Comparable {
}

export class ComponentRepository extends SortableSet<Component> {
* [treeIterator] (): Generator<Component> {
for (const component of this) {
yield component
yield * component.components[treeIterator]()
}
}
}
11 changes: 9 additions & 2 deletions src/serialize/json/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as Models from '../../models'
import { Protocol as Spec, Version as SpecVersion } from '../../spec'
import { NormalizerOptions } from '../types'
import { JsonSchema, Normalized } from './types'
import { treeIterator } from '../../helpers/tree'

export class Factory {
readonly #spec: Spec
Expand Down Expand Up @@ -270,6 +271,9 @@ export class ComponentNormalizer extends Base {
: this._factory.makeForSWID().normalize(data.swid, options),
externalReferences: data.externalReferences.size > 0
? this._factory.makeForExternalReference().normalizeRepository(data.externalReferences, options)
: undefined,
components: data.components.size > 0
? this.normalizeRepository(data.components, options)
: undefined
}
: undefined
Expand Down Expand Up @@ -394,9 +398,12 @@ export class DependencyGraphNormalizer extends Base {
const allRefs = new Map<Models.BomRef, Models.BomRefRepository>()
if (data.metadata.component !== undefined) {
allRefs.set(data.metadata.component.bomRef, data.metadata.component.dependencies)
for (const component of data.metadata.component.components[treeIterator]()) {
allRefs.set(component.bomRef, component.dependencies)
}
}
for (const c of data.components) {
allRefs.set(c.bomRef, new Models.BomRefRepository(c.dependencies))
for (const component of data.components[treeIterator]()) {
allRefs.set(component.bomRef, component.dependencies)
}

const normalized: Normalized.Dependency[] = []
Expand Down
18 changes: 15 additions & 3 deletions src/serialize/xml/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as Models from '../../models'
import { Protocol as Spec, Version as SpecVersion } from '../../spec'
import { NormalizerOptions } from '../types'
import { SimpleXml, XmlSchema } from './types'
import { treeIterator } from '../../helpers/tree'

export class Factory {
readonly #spec: Spec
Expand Down Expand Up @@ -335,6 +336,13 @@ export class ComponentNormalizer extends Base {
.normalizeRepository(data.externalReferences, options, 'reference')
}
: undefined
const components: SimpleXml.Element | undefined = data.components.size > 0
? {
type: 'element',
name: 'components',
children: this.normalizeRepository(data.components, options, 'component')
}
: undefined
return {
type: 'element',
name: elementName,
Expand All @@ -357,7 +365,8 @@ export class ComponentNormalizer extends Base {
makeOptionalTextElement(data.cpe, 'cpe'),
makeOptionalTextElement(data.purl, 'purl'),
swid,
extRefs
extRefs,
components
].filter(isNotUndefined)
}
}
Expand Down Expand Up @@ -509,9 +518,12 @@ export class DependencyGraphNormalizer extends Base {
const allRefs = new Map<Models.BomRef, Models.BomRefRepository>()
if (data.metadata.component !== undefined) {
allRefs.set(data.metadata.component.bomRef, data.metadata.component.dependencies)
for (const component of data.metadata.component.components[treeIterator]()) {
allRefs.set(component.bomRef, component.dependencies)
}
}
for (const c of data.components) {
allRefs.set(c.bomRef, new Models.BomRefRepository(c.dependencies))
for (const component of data.components[treeIterator]()) {
allRefs.set(component.bomRef, component.dependencies)
}

const normalized: Array<(SimpleXml.Element & { attributes: { ref: string } })> = []
Expand Down
29 changes: 26 additions & 3 deletions tests/_data/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ module.exports.createComplexStructure = function () {
component.scope = Enums.ComponentScope.Required
component.supplier = new Models.OrganizationalEntity({ name: 'Component Supplier' })
component.supplier.url.add(new URL('https://localhost/componentSupplier-B'))
component.supplier.url.add(new URL('https://localhost/componentSupplier-A'))
component.supplier.url.add('https://localhost/componentSupplier-A')
component.supplier.contact.add(new Models.OrganizationalContact({ name: 'The quick brown fox' }))
component.supplier.contact.add((function (contact) {
contact.name = 'Franz'
Expand All @@ -150,18 +150,41 @@ module.exports.createComplexStructure = function () {
return component
})(new Models.Component(Enums.ComponentType.Library, 'dummy-component', { version: '1337-beta' })))

bom.components.add(function (component) {
bom.components.add((function (component) {
// interlink everywhere
bom.metadata.component.dependencies.add(component.bomRef)
bom.components.forEach(c => c.dependencies.add(component.bomRef))
return component
}(new Models.Component(Enums.ComponentType.Library, 'a-component', {
})(new Models.Component(Enums.ComponentType.Library, 'a-component', {
bomRef: 'a-component',
version: '', // empty string - not undefined
dependencies: new Models.BomRefRepository([
new Models.BomRef('unknown foreign ref that should not be rendered')
])
})))

bom.components.add((function (component) {
// scenario:
// * `subComponentA` is a bundled dependency, that itself depends on `subComponentB`.
// * `subComponentB` is a transitive bundled dependency.
const subComponentA = new Models.Component(Enums.ComponentType.Library, 'SubComponentA', {
bomRef: `${component.bomRef.value}#SubComponentA`
})
component.dependencies.add(subComponentA.bomRef)
component.components.add(subComponentA)
const subComponentB = new Models.Component(Enums.ComponentType.Library, 'SubComponentB', {
bomRef: `${component.bomRef.value}#SubComponentB`
})
subComponentA.dependencies.add(subComponentB.bomRef)
component.components.add(subComponentB)

bom.metadata.component.dependencies.add(component.bomRef)

return component
})(new Models.Component(
Enums.ComponentType.Framework, 'SomeFrameworkBundle', {
bomRef: 'SomeFrameworkBundle'
})))

return bom
}
38 changes: 37 additions & 1 deletion tests/_data/normalizeResults/json_sortedLists_spec1.2.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 37 additions & 1 deletion tests/_data/normalizeResults/json_sortedLists_spec1.3.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 34 additions & 1 deletion tests/_data/normalizeResults/json_sortedLists_spec1.4.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading