Skip to content

Commit 5ff1d20

Browse files
authored
feat: tools as Components & Services (#1163)
* BREAKING changes * Property `Models.Bom.tools` is an instance of `Models.Tools` now ([#1152] via [#1163]) Before, it was an instance of `Models.ToolRepository`. * Added * Static function `Models.Tool.fromComponent()` (via [#1163]) * Static function `Models.Tool.fromService()` (via [#1163]) * New class `Models.Tools` ([#1152] via [#1163]) * New serialization/normalization for `Models.Tools` ([#1152] via [#1163]) * Changed * Serializers and `Bom`-Normalizers will take changed `Models.Bom.tools` into account ([#1152] via [#1163]) ---- fixes #1152 as described here in #1152 (comment) --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent ccdd7f5 commit 5ff1d20

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1711
-460
lines changed

HISTORY.md

+14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ All notable changes to this project will be documented in this file.
66

77
<!-- add unreleased items here -->
88

9+
* BREAKING changes
10+
* Property `Models.Bom.tools` is an instance of `Models.Tools` now ([#1152] via [#1163])
11+
Before, it was an instance of `Models.ToolRepository`.
12+
* Added
13+
* Static function `Models.Tool.fromComponent()` (via [#1163])
14+
* Static function `Models.Tool.fromService()` (via [#1163])
15+
* New class `Models.Tools` ([#1152] via [#1163])
16+
* New serialization/normalization for `Models.Tools` ([#1152] via [#1163])
17+
* Changed
18+
* Serializers and `Bom`-Normalizers will take changed `Models.Bom.tools` into account ([#1152] via [#1163])
19+
20+
[#1152]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/1152
21+
[#1163]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1163
22+
923
## 6.12.0 -- 2024-11-12
1024

1125
* Added

src/_helpers/iterable.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 function * chainI<T> (...iterables: Array<Iterable<T>>): Generator<T> {
21+
for (const iterable of iterables) {
22+
for (const item of iterable) {
23+
yield item
24+
}
25+
}
26+
}

src/models/metadata.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { LifecycleRepository } from './lifecycle'
2323
import { OrganizationalContactRepository } from './organizationalContact'
2424
import type { OrganizationalEntity } from './organizationalEntity'
2525
import { PropertyRepository } from './property'
26-
import { ToolRepository } from './tool'
26+
import { Tools } from './tool'
2727

2828
export interface OptionalMetadataProperties {
2929
timestamp?: Metadata['timestamp']
@@ -40,7 +40,7 @@ export interface OptionalMetadataProperties {
4040
export class Metadata {
4141
timestamp?: Date
4242
lifecycles: LifecycleRepository
43-
tools: ToolRepository
43+
tools: Tools
4444
authors: OrganizationalContactRepository
4545
component?: Component
4646
manufacture?: OrganizationalEntity
@@ -51,7 +51,7 @@ export class Metadata {
5151
constructor (op: OptionalMetadataProperties = {}) {
5252
this.timestamp = op.timestamp
5353
this.lifecycles = op.lifecycles ?? new LifecycleRepository()
54-
this.tools = op.tools ?? new ToolRepository()
54+
this.tools = op.tools ?? new Tools()
5555
this.authors = op.authors ?? new OrganizationalContactRepository()
5656
this.component = op.component
5757
this.manufacture = op.manufacture

src/models/tool.ts

+48
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
1919

2020
import type { Comparable } from '../_helpers/sortable'
2121
import { SortableComparables } from '../_helpers/sortable'
22+
import type { Component } from "./component";
23+
import { ComponentRepository} from "./component";
2224
import { ExternalReferenceRepository } from './externalReference'
2325
import { HashDictionary } from './hash'
26+
import type { Service } from "./service";
27+
import { ServiceRepository } from "./service";
2428

2529
export interface OptionalToolProperties {
2630
vendor?: Tool['vendor']
@@ -53,7 +57,51 @@ export class Tool implements Comparable<Tool> {
5357
(this.version ?? '').localeCompare(other.version ?? '')
5458
/* eslint-enable @typescript-eslint/strict-boolean-expressions */
5559
}
60+
61+
static fromComponent(component: Component): Tool {
62+
return new Tool({
63+
vendor: component.group,
64+
name: component.name,
65+
version: component.version,
66+
hashes: component.hashes,
67+
externalReferences: component.externalReferences
68+
})
69+
}
70+
71+
static fromService(service: Service): Tool {
72+
return new Tool({
73+
vendor: service.group,
74+
name: service.name,
75+
version: service.version,
76+
externalReferences: service.externalReferences
77+
})
78+
}
5679
}
5780

5881
export class ToolRepository extends SortableComparables<Tool> {
5982
}
83+
84+
85+
export interface OptionalToolsProperties {
86+
components?: Tools['components']
87+
services?: Tools['services']
88+
tools?: Tools['tools']
89+
}
90+
91+
export class Tools {
92+
components: ComponentRepository
93+
services: ServiceRepository
94+
tools: ToolRepository
95+
96+
constructor(op: OptionalToolsProperties = {}) {
97+
this.components = op.components ?? new ComponentRepository()
98+
this.services = op.services ?? new ServiceRepository()
99+
this.tools = op.tools ?? new ToolRepository()
100+
}
101+
102+
get size(): number {
103+
return this.components.size
104+
+ this.services.size
105+
+ this.tools.size
106+
}
107+
}

src/models/vulnerability/vulnerability.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { SortableComparables } from '../../_helpers/sortable'
2222
import { CweRepository } from '../../types/cwe'
2323
import { BomRef } from '../bomRef'
2424
import { PropertyRepository } from '../property'
25-
import { ToolRepository } from '../tool'
25+
import { Tools } from '../tool'
2626
import { AdvisoryRepository } from './advisory'
2727
import { AffectRepository } from './affect'
2828
import type { Analysis } from './analysis'
@@ -68,7 +68,7 @@ export class Vulnerability implements Comparable<Vulnerability> {
6868
published?: Date
6969
updated?: Date
7070
credits?: Credits
71-
tools: ToolRepository
71+
tools: Tools
7272
analysis?: Analysis
7373
affects: AffectRepository
7474
properties: PropertyRepository
@@ -88,7 +88,7 @@ export class Vulnerability implements Comparable<Vulnerability> {
8888
this.published = op.published
8989
this.updated = op.updated
9090
this.credits = op.credits
91-
this.tools = op.tools ?? new ToolRepository()
91+
this.tools = op.tools ?? new Tools()
9292
this.analysis = op.analysis
9393
this.affects = op.affects ?? new AffectRepository()
9494
this.properties = op.properties ?? new PropertyRepository()

src/serialize/json/normalize.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ SPDX-License-Identifier: Apache-2.0
1717
Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

20+
import { chainI } from "../../_helpers/iterable";
2021
import { isNotUndefined } from '../../_helpers/notUndefined'
2122
import type { SortableIterable } from '../../_helpers/sortable'
2223
import type { Stringable } from '../../_helpers/stringable'
@@ -25,6 +26,7 @@ import { escapeUri } from '../../_helpers/uri'
2526
import type * as Models from '../../models'
2627
import { LicenseExpression, NamedLicense, SpdxLicense } from '../../models/license'
2728
import { NamedLifecycle } from '../../models/lifecycle'
29+
import { Tool, ToolRepository } from '../../models/tool'
2830
import { AffectedSingleVersion, AffectedVersionRange } from '../../models/vulnerability/affect'
2931
import { isSupportedSpdxId } from '../../spdx'
3032
import type { _SpecProtocol as Spec } from '../../spec/_protocol'
@@ -72,6 +74,10 @@ export class Factory {
7274
return new ToolNormalizer(this)
7375
}
7476

77+
makeForTools (): ToolsNormalizer {
78+
return new ToolsNormalizer(this)
79+
}
80+
7581
makeForOrganizationalContact (): OrganizationalContactNormalizer {
7682
return new OrganizationalContactNormalizer(this)
7783
}
@@ -221,7 +227,7 @@ export class MetadataNormalizer extends BaseJsonNormalizer<Models.Metadata> {
221227
? this._factory.makeForLifecycle().normalizeIterable(data.lifecycles, options)
222228
: undefined,
223229
tools: data.tools.size > 0
224-
? this._factory.makeForTool().normalizeIterable(data.tools, options)
230+
? this._factory.makeForTools().normalize(data.tools, options)
225231
: undefined,
226232
authors: data.authors.size > 0
227233
? this._factory.makeForOrganizationalContact().normalizeIterable(data.authors, options)
@@ -285,6 +291,23 @@ export class ToolNormalizer extends BaseJsonNormalizer<Models.Tool> {
285291
}
286292
}
287293

294+
export class ToolsNormalizer extends BaseJsonNormalizer<Models.Tools> {
295+
normalize(data: Models.Tools, options: NormalizerOptions): Normalized.ToolsType {
296+
if (data.tools.size > 0 || !this._factory.spec.supportsToolsComponentsServices) {
297+
return this._factory.makeForTool().normalizeIterable(
298+
new ToolRepository(chainI<Models.Tool>(
299+
Array.from(data.components, Tool.fromComponent),
300+
Array.from(data.services, Tool.fromService),
301+
data.tools,
302+
)), options)
303+
}
304+
return {
305+
components: this._factory.makeForComponent().normalizeIterable(data.components, options),
306+
services: this._factory.makeForService().normalizeIterable(data.services, options)
307+
}
308+
}
309+
}
310+
288311
export class HashNormalizer extends BaseJsonNormalizer<Models.Hash> {
289312
normalize ([algorithm, content]: Models.Hash, options: NormalizerOptions): Normalized.Hash | undefined {
290313
const spec = this._factory.spec
@@ -723,7 +746,7 @@ export class VulnerabilityNormalizer extends BaseJsonNormalizer<Models.Vulnerabi
723746
? undefined
724747
: this._factory.makeForVulnerabilityCredits().normalize(data.credits, options),
725748
tools: data.tools.size > 0
726-
? this._factory.makeForTool().normalizeIterable(data.tools, options)
749+
? this._factory.makeForTools().normalize(data.tools, options)
727750
: undefined,
728751
analysis: data.analysis === undefined
729752
? undefined

src/serialize/json/types.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export namespace Normalized {
9090
export interface Metadata {
9191
timestamp?: JsonSchema.DateTime
9292
lifecycles?: Lifecycle[]
93-
tools?: Tool[]
93+
tools?: ToolsType
9494
authors?: OrganizationalContact[]
9595
component?: Component
9696
manufacture?: OrganizationalEntity
@@ -118,6 +118,14 @@ export namespace Normalized {
118118
externalReferences?: ExternalReference[]
119119
}
120120

121+
/** since CDX 1.5 */
122+
export interface Tools {
123+
components: Component[]
124+
services: Service[]
125+
}
126+
127+
export type ToolsType = Tools | Tool[]
128+
121129
export interface OrganizationalContact {
122130
name?: string
123131
email?: JsonSchema.IdnEmail
@@ -257,7 +265,7 @@ export namespace Normalized {
257265
published?: JsonSchema.DateTime
258266
updated?: JsonSchema.DateTime
259267
credits?: Vulnerability.Credits
260-
tools?: Tool[]
268+
tools?: ToolsType
261269
analysis?: Vulnerability.Analysis
262270
affects?: Vulnerability.Affect[]
263271
properties?: Property[]

src/serialize/xml/normalize.ts

+42-10
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ SPDX-License-Identifier: Apache-2.0
1717
Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

20+
import { chainI } from "../../_helpers/iterable";
2021
import { isNotUndefined } from '../../_helpers/notUndefined'
2122
import type { SortableIterable } from '../../_helpers/sortable'
2223
import type { Stringable } from '../../_helpers/stringable'
2324
import { treeIteratorSymbol } from '../../_helpers/tree'
2425
import { escapeUri } from '../../_helpers/uri'
2526
import type * as Models from '../../models'
27+
import { Tool, ToolRepository } from "../../models";
2628
import { LicenseExpression, NamedLicense, SpdxLicense } from '../../models/license'
2729
import { NamedLifecycle } from '../../models/lifecycle'
2830
import { AffectedSingleVersion, AffectedVersionRange } from '../../models/vulnerability/affect'
@@ -75,6 +77,10 @@ export class Factory {
7577
return new ToolNormalizer(this)
7678
}
7779

80+
makeForTools (): ToolsNormalizer {
81+
return new ToolsNormalizer(this)
82+
}
83+
7884
makeForOrganizationalContact (): OrganizationalContactNormalizer {
7985
return new OrganizationalContactNormalizer(this)
8086
}
@@ -250,11 +256,7 @@ export class MetadataNormalizer extends BaseXmlNormalizer<Models.Metadata> {
250256
}
251257
: undefined
252258
const tools: SimpleXml.Element | undefined = data.tools.size > 0
253-
? {
254-
type: 'element',
255-
name: 'tools',
256-
children: this._factory.makeForTool().normalizeIterable(data.tools, options, 'tool')
257-
}
259+
? this._factory.makeForTools().normalize(data.tools, options, 'tools')
258260
: undefined
259261
const authors: SimpleXml.Element | undefined = data.authors.size > 0
260262
? {
@@ -369,6 +371,40 @@ export class ToolNormalizer extends BaseXmlNormalizer<Models.Tool> {
369371
}
370372
}
371373

374+
export class ToolsNormalizer extends BaseXmlNormalizer<Models.Tools> {
375+
normalize (data: Models.Tools, options: NormalizerOptions, elementName: string): SimpleXml.Element {
376+
let children: SimpleXml.Element[] = []
377+
if (data.tools.size > 0 || !this._factory.spec.supportsToolsComponentsServices) {
378+
children = this._factory.makeForTool().normalizeIterable(
379+
new ToolRepository(chainI(
380+
Array.from(data.components, Tool.fromComponent),
381+
Array.from(data.services, Tool.fromService),
382+
data.tools,
383+
)), options, 'tool')
384+
} else {
385+
if (data.components.size > 0) {
386+
children.push({
387+
type: 'element',
388+
name: 'components',
389+
children: this._factory.makeForComponent().normalizeIterable(data.components, options, 'component')
390+
})
391+
}
392+
if (data.components.size > 0) {
393+
children.push({
394+
type: 'element',
395+
name: 'services',
396+
children: this._factory.makeForService().normalizeIterable(data.services, options, 'service')
397+
})
398+
}
399+
}
400+
return {
401+
type: 'element',
402+
name: elementName,
403+
children
404+
}
405+
}
406+
}
407+
372408
export class HashNormalizer extends BaseXmlNormalizer<Models.Hash> {
373409
normalize ([algorithm, content]: Models.Hash, options: NormalizerOptions, elementName: string): SimpleXml.Element | undefined {
374410
const spec = this._factory.spec
@@ -935,11 +971,7 @@ export class VulnerabilityNormalizer extends BaseXmlNormalizer<Models.Vulnerabil
935971
}
936972
: undefined
937973
const tools: SimpleXml.Element | undefined = data.tools.size > 0
938-
? {
939-
type: 'element',
940-
name: 'tools',
941-
children: this._factory.makeForTool().normalizeIterable(data.tools, options, 'tool')
942-
}
974+
? this._factory.makeForTools().normalize(data.tools, options, 'tools')
943975
: undefined
944976
const affects: SimpleXml.Element | undefined = data.affects.size > 0
945977
? {

0 commit comments

Comments
 (0)