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
@@ -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

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
@@ -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']
@@ -45,6 +46,7 @@ interface OptionalProperties {
swid?: Component['swid']
version?: Component['version']
dependencies?: Component['dependencies']
components?: Component['components']
cpe?: Component['cpe']
}

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

/** @see bomRef */
readonly #bomRef: BomRef
@@ -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
}

@@ -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
@@ -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
@@ -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
@@ -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[] = []
18 changes: 15 additions & 3 deletions src/serialize/xml/normalize.ts
Original file line number Diff line number Diff line change
@@ -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
@@ -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,
@@ -357,7 +365,8 @@ export class ComponentNormalizer extends Base {
makeOptionalTextElement(data.cpe, 'cpe'),
makeOptionalTextElement(data.purl, 'purl'),
swid,
extRefs
extRefs,
components
].filter(isNotUndefined)
}
}
@@ -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 } })> = []
29 changes: 26 additions & 3 deletions tests/_data/models.js
Original file line number Diff line number Diff line change
@@ -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'
@@ -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.

113 changes: 113 additions & 0 deletions tests/_data/normalizeResults/xml_sortedLists_spec1.2.json

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

113 changes: 113 additions & 0 deletions tests/_data/normalizeResults/xml_sortedLists_spec1.3.json

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

98 changes: 98 additions & 0 deletions tests/_data/normalizeResults/xml_sortedLists_spec1.4.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/serializeResults/json_complex_spec1.2.json.bin

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/serializeResults/json_complex_spec1.3.json.bin

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/serializeResults/json_complex_spec1.4.json.bin

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

22 changes: 22 additions & 0 deletions tests/_data/serializeResults/xml_complex_spec1.2.xml.bin

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

22 changes: 22 additions & 0 deletions tests/_data/serializeResults/xml_complex_spec1.3.xml.bin

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

19 changes: 19 additions & 0 deletions tests/_data/serializeResults/xml_complex_spec1.4.xml.bin

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

7 changes: 4 additions & 3 deletions tests/integration/Serialize.JsonNormalize.test.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ const { describe, beforeEach, afterEach, it } = require('mocha')
const { createComplexStructure } = require('../_data/models')
const { loadNormalizeResult } = require('../_data/normalize')
/* uncomment next line to dump data */
// const { writeNormalizeResult } = require('../_data/normalize')
const { writeNormalizeResult } = require('../_data/normalize')

const {
Serialize: {
@@ -63,8 +63,9 @@ describe('Serialize.JsonNormalize', function () {

const json = JSON.stringify(normalized, null, 2)

/* uncomment next line to dump data */
// writeNormalizeResult(json, 'json_sortedLists', spec.version, 'json')
if (process.env.CJL_TEST_UPDATE_SNAPSHOTS) {
writeNormalizeResult(json, 'json_sortedLists', spec.version, 'json')
}

assert.deepStrictEqual(
JSON.parse(json),
7 changes: 4 additions & 3 deletions tests/integration/Serialize.JsonSerialize.test.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ const { describe, beforeEach, afterEach, it } = require('mocha')
const { createComplexStructure } = require('../_data/models')
const { loadSerializeResult } = require('../_data/serialize')
/* uncomment next line to dump data */
// const { writeSerializeResult } = require('../_data/serialize')
const { writeSerializeResult } = require('../_data/serialize')

const {
Serialize: {
@@ -60,8 +60,9 @@ describe('Serialize.JsonSerialize', function () {
space: 4
})

/* uncomment next line to dump data */
// writeSerializeResult(serialized, 'json_complex', spec.version, 'json')
if (process.env.CJL_TEST_UPDATE_SNAPSHOTS) {
writeSerializeResult(serialized, 'json_complex', spec.version, 'json')
}

assert.strictEqual(
serialized,
7 changes: 4 additions & 3 deletions tests/integration/Serialize.XmlNormalize.test.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ const { describe, beforeEach, afterEach, it } = require('mocha')
const { createComplexStructure } = require('../_data/models')
const { loadNormalizeResult } = require('../_data/normalize')
/* uncomment next line to dump data */
// const { writeNormalizeResult } = require('../_data/normalize')
const { writeNormalizeResult } = require('../_data/normalize')

const {
Serialize: {
@@ -63,8 +63,9 @@ describe('Serialize.XmlNormalize', function () {

const json = JSON.stringify(normalized, null, 2)

/* uncomment next line to dump data */
// writeNormalizeResult(json, 'xml_sortedLists', spec.version, 'json')
if (process.env.CJL_TEST_UPDATE_SNAPSHOTS) {
writeNormalizeResult(json, 'xml_sortedLists', spec.version, 'json')
}

assert.deepStrictEqual(
JSON.parse(json),
7 changes: 4 additions & 3 deletions tests/integration/Serialize.XmlSerialize.test.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ const { describe, beforeEach, afterEach, it } = require('mocha')
const { createComplexStructure } = require('../_data/models')
const { loadSerializeResult } = require('../_data/serialize')
/* uncomment next line to dump data */
// const { writeSerializeResult } = require('../_data/serialize')
const { writeSerializeResult } = require('../_data/serialize')

const {
Serialize: {
@@ -60,8 +60,9 @@ describe('Serialize.XmlSerialize', function () {
space: 4
})

/* uncomment next line to dump data */
// writeSerializeResult(serialized, 'xml_complex', spec.version, 'xml')
if (process.env.CJL_TEST_UPDATE_SNAPSHOTS) {
writeSerializeResult(serialized, 'xml_complex', spec.version, 'xml')
}

assert.strictEqual(
serialized,
15 changes: 10 additions & 5 deletions tests/unit/Models.Component.spec.js
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ const { PackageURL } = require('packageurl-js')

const {
Models: {
Component,
Component, ComponentRepository,
BomRef, BomRefRepository,
ExternalReferenceRepository, ExternalReference,
HashRepository,
@@ -57,22 +57,24 @@ suite('Models.Component', () => {
assert.strictEqual(component.supplier, undefined)
assert.strictEqual(component.swid, undefined)
assert.strictEqual(component.version, undefined)
assert.strictEqual(component.components.size, 0)
})

test('constructor with OptionalProperties', () => {
const dummnBomRef = new BomRef('testing')
const dummyBomRef = new BomRef('testing')
const dummyExtRef = new ExternalReference('../', 'other')
const dummyLicense = new NamedLicense('mine')
const dummyPurl = new PackageURL('npm', 'ns', 'app', '1.33.7', {}, undefined)
const dummySupplier = new OrganizationalEntity({ name: 'dummySupplier' })
const dummySWID = new SWID('my-fake-swid', 'foo-bar')
const subComponent = new Component('library', 'MySubComponent')

const component = new Component('application', 'foobar', {
author: 'my author',
bomRef: 'my-bomref',
copyright: 'my copyright',
cpe: 'cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*',
dependencies: new BomRefRepository([dummnBomRef]),
dependencies: new BomRefRepository([dummyBomRef]),
description: 'this is a test',
externalReferences: new ExternalReferenceRepository([dummyExtRef]),
group: 'the-crew',
@@ -82,7 +84,8 @@ suite('Models.Component', () => {
scope: 'optional',
supplier: dummySupplier,
swid: dummySWID,
version: '1.33.7'
version: '1.33.7',
components: new ComponentRepository([subComponent])
})

assert.strictEqual(component.type, 'application')
@@ -92,7 +95,7 @@ suite('Models.Component', () => {
assert.strictEqual(component.copyright, 'my copyright')
assert.strictEqual(component.cpe, 'cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*')
assert.strictEqual(component.dependencies.size, 1)
assert.strictEqual(Array.from(component.dependencies)[0], dummnBomRef)
assert.strictEqual(Array.from(component.dependencies)[0], dummyBomRef)
assert.strictEqual(component.description, 'this is a test')
assert.strictEqual(component.externalReferences.size, 1)
assert.strictEqual(Array.from(component.externalReferences)[0], dummyExtRef)
@@ -106,5 +109,7 @@ suite('Models.Component', () => {
assert.strictEqual(component.supplier, dummySupplier)
assert.strictEqual(component.swid, dummySWID)
assert.strictEqual(component.version, '1.33.7')
assert.strictEqual(component.components.size, 1)
assert.strictEqual(Array.from(component.components)[0], subComponent)
})
})