Skip to content

Commit 9a12342

Browse files
authored
add support for sub-components (Models.Component.components) (#136)
* Support for nested/bundled (sub-)components via `Models.Component.components` was added, including serialization/normalization of models and impact on dependency graphs rendering. Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 7d9c3ec commit 9a12342

23 files changed

+717
-35
lines changed

HISTORY.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ All notable changes to this project will be documented in this file.
55
## unreleased
66

77
* Added
8-
* CycloneDX spec version 1.4 made element `bom.component.version` optional.
9-
Therefore, serialization/normalization with this spec version will no longer render this element,
10-
when its value is empty. (via [#137], [#138])
11-
8+
* Support for nested/bundled (sub-)components via `Models.Component.components` was added, including
9+
serialization/normalization of models and impact on dependency graphs rendering. ([#132] via [#136])
10+
* CycloneDX spec version 1.4 made element `Models.Component.version` optional.
11+
Therefore, serialization/normalization with this spec version will no longer render this element
12+
if its value is empty. (via [#137], [#138])
13+
14+
[#132]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/132
15+
[#136]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/136
1216
[#137]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/137
1317
[#138]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/138
1418

src/helpers/tree.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*!
2+
This file is part of CycloneDX JavaScript Library.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache-2.0
17+
Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
20+
export const treeIterator = Symbol('iterator of a tree/nesting-like structure')

src/models/component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ExternalReferenceRepository } from './externalReference'
2828
import { LicenseRepository } from './license'
2929
import { SWID } from './swid'
3030
import { Comparable, SortableSet } from '../helpers/sortableSet'
31+
import { treeIterator } from '../helpers/tree'
3132

3233
interface OptionalProperties {
3334
bomRef?: BomRef['value']
@@ -45,6 +46,7 @@ interface OptionalProperties {
4546
swid?: Component['swid']
4647
version?: Component['version']
4748
dependencies?: Component['dependencies']
49+
components?: Component['components']
4850
cpe?: Component['cpe']
4951
}
5052

@@ -65,6 +67,7 @@ export class Component implements Comparable {
6567
swid?: SWID
6668
version?: string
6769
dependencies: BomRefRepository
70+
components: ComponentRepository
6871

6972
/** @see bomRef */
7073
readonly #bomRef: BomRef
@@ -93,6 +96,7 @@ export class Component implements Comparable {
9396
this.version = op.version
9497
this.description = op.description
9598
this.dependencies = op.dependencies ?? new BomRefRepository()
99+
this.components = op.components ?? new ComponentRepository()
96100
this.cpe = op.cpe
97101
}
98102

@@ -134,4 +138,10 @@ export class Component implements Comparable {
134138
}
135139

136140
export class ComponentRepository extends SortableSet<Component> {
141+
* [treeIterator] (): Generator<Component> {
142+
for (const component of this) {
143+
yield component
144+
yield * component.components[treeIterator]()
145+
}
146+
}
137147
}

src/serialize/json/normalize.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import * as Models from '../../models'
2323
import { Protocol as Spec, Version as SpecVersion } from '../../spec'
2424
import { NormalizerOptions } from '../types'
2525
import { JsonSchema, Normalized } from './types'
26+
import { treeIterator } from '../../helpers/tree'
2627

2728
export class Factory {
2829
readonly #spec: Spec
@@ -270,6 +271,9 @@ export class ComponentNormalizer extends Base {
270271
: this._factory.makeForSWID().normalize(data.swid, options),
271272
externalReferences: data.externalReferences.size > 0
272273
? this._factory.makeForExternalReference().normalizeRepository(data.externalReferences, options)
274+
: undefined,
275+
components: data.components.size > 0
276+
? this.normalizeRepository(data.components, options)
273277
: undefined
274278
}
275279
: undefined
@@ -394,9 +398,12 @@ export class DependencyGraphNormalizer extends Base {
394398
const allRefs = new Map<Models.BomRef, Models.BomRefRepository>()
395399
if (data.metadata.component !== undefined) {
396400
allRefs.set(data.metadata.component.bomRef, data.metadata.component.dependencies)
401+
for (const component of data.metadata.component.components[treeIterator]()) {
402+
allRefs.set(component.bomRef, component.dependencies)
403+
}
397404
}
398-
for (const c of data.components) {
399-
allRefs.set(c.bomRef, new Models.BomRefRepository(c.dependencies))
405+
for (const component of data.components[treeIterator]()) {
406+
allRefs.set(component.bomRef, component.dependencies)
400407
}
401408

402409
const normalized: Normalized.Dependency[] = []

src/serialize/xml/normalize.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import * as Models from '../../models'
2323
import { Protocol as Spec, Version as SpecVersion } from '../../spec'
2424
import { NormalizerOptions } from '../types'
2525
import { SimpleXml, XmlSchema } from './types'
26+
import { treeIterator } from '../../helpers/tree'
2627

2728
export class Factory {
2829
readonly #spec: Spec
@@ -335,6 +336,13 @@ export class ComponentNormalizer extends Base {
335336
.normalizeRepository(data.externalReferences, options, 'reference')
336337
}
337338
: undefined
339+
const components: SimpleXml.Element | undefined = data.components.size > 0
340+
? {
341+
type: 'element',
342+
name: 'components',
343+
children: this.normalizeRepository(data.components, options, 'component')
344+
}
345+
: undefined
338346
return {
339347
type: 'element',
340348
name: elementName,
@@ -357,7 +365,8 @@ export class ComponentNormalizer extends Base {
357365
makeOptionalTextElement(data.cpe, 'cpe'),
358366
makeOptionalTextElement(data.purl, 'purl'),
359367
swid,
360-
extRefs
368+
extRefs,
369+
components
361370
].filter(isNotUndefined)
362371
}
363372
}
@@ -509,9 +518,12 @@ export class DependencyGraphNormalizer extends Base {
509518
const allRefs = new Map<Models.BomRef, Models.BomRefRepository>()
510519
if (data.metadata.component !== undefined) {
511520
allRefs.set(data.metadata.component.bomRef, data.metadata.component.dependencies)
521+
for (const component of data.metadata.component.components[treeIterator]()) {
522+
allRefs.set(component.bomRef, component.dependencies)
523+
}
512524
}
513-
for (const c of data.components) {
514-
allRefs.set(c.bomRef, new Models.BomRefRepository(c.dependencies))
525+
for (const component of data.components[treeIterator]()) {
526+
allRefs.set(component.bomRef, component.dependencies)
515527
}
516528

517529
const normalized: Array<(SimpleXml.Element & { attributes: { ref: string } })> = []

tests/_data/models.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ module.exports.createComplexStructure = function () {
128128
component.scope = Enums.ComponentScope.Required
129129
component.supplier = new Models.OrganizationalEntity({ name: 'Component Supplier' })
130130
component.supplier.url.add(new URL('https://localhost/componentSupplier-B'))
131-
component.supplier.url.add(new URL('https://localhost/componentSupplier-A'))
131+
component.supplier.url.add('https://localhost/componentSupplier-A')
132132
component.supplier.contact.add(new Models.OrganizationalContact({ name: 'The quick brown fox' }))
133133
component.supplier.contact.add((function (contact) {
134134
contact.name = 'Franz'
@@ -150,18 +150,41 @@ module.exports.createComplexStructure = function () {
150150
return component
151151
})(new Models.Component(Enums.ComponentType.Library, 'dummy-component', { version: '1337-beta' })))
152152

153-
bom.components.add(function (component) {
153+
bom.components.add((function (component) {
154154
// interlink everywhere
155155
bom.metadata.component.dependencies.add(component.bomRef)
156156
bom.components.forEach(c => c.dependencies.add(component.bomRef))
157157
return component
158-
}(new Models.Component(Enums.ComponentType.Library, 'a-component', {
158+
})(new Models.Component(Enums.ComponentType.Library, 'a-component', {
159159
bomRef: 'a-component',
160160
version: '', // empty string - not undefined
161161
dependencies: new Models.BomRefRepository([
162162
new Models.BomRef('unknown foreign ref that should not be rendered')
163163
])
164164
})))
165165

166+
bom.components.add((function (component) {
167+
// scenario:
168+
// * `subComponentA` is a bundled dependency, that itself depends on `subComponentB`.
169+
// * `subComponentB` is a transitive bundled dependency.
170+
const subComponentA = new Models.Component(Enums.ComponentType.Library, 'SubComponentA', {
171+
bomRef: `${component.bomRef.value}#SubComponentA`
172+
})
173+
component.dependencies.add(subComponentA.bomRef)
174+
component.components.add(subComponentA)
175+
const subComponentB = new Models.Component(Enums.ComponentType.Library, 'SubComponentB', {
176+
bomRef: `${component.bomRef.value}#SubComponentB`
177+
})
178+
subComponentA.dependencies.add(subComponentB.bomRef)
179+
component.components.add(subComponentB)
180+
181+
bom.metadata.component.dependencies.add(component.bomRef)
182+
183+
return component
184+
})(new Models.Component(
185+
Enums.ComponentType.Framework, 'SomeFrameworkBundle', {
186+
bomRef: 'SomeFrameworkBundle'
187+
})))
188+
166189
return bom
167190
}

tests/_data/normalizeResults/json_sortedLists_spec1.2.json

Lines changed: 37 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/normalizeResults/json_sortedLists_spec1.3.json

Lines changed: 37 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/normalizeResults/json_sortedLists_spec1.4.json

Lines changed: 34 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)