Skip to content

Commit 7cb7699

Browse files
authored
feat: basic support for Services (#1165)
fixes #1164 --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 40eb623 commit 7cb7699

32 files changed

+2217
-29
lines changed

HISTORY.md

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

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

9+
* Added
10+
* Support for services ([#1164] via [#1165])
11+
* New class `Models.Service` ([#1164] via [#1165])
12+
* New class `Models.ServiceRepository` ([#1164] via [#1165])
13+
* Class `Models.Bom` got new property `services` ([#1164] via [#1165])
14+
* Serializers and `Bom`-Normalizers will take `Models.Bom.services` into account ([#1164] via [#1165])
15+
16+
[#1164]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/1164
17+
[#1165]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1165
18+
919
## 6.11.1 -- 2024-10-24
1020

1121
* Fixed

src/models/bom.ts

+4
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import type { PositiveInteger } from '../types/integer'
2121
import { isPositiveInteger } from '../types/integer'
2222
import { ComponentRepository } from './component'
2323
import { Metadata } from './metadata'
24+
import { ServiceRepository } from './service'
2425
import { VulnerabilityRepository } from './vulnerability/vulnerability'
2526

2627
export interface OptionalBomProperties {
2728
metadata?: Bom['metadata']
2829
components?: Bom['components']
30+
services?: Bom['services']
2931
version?: Bom['version']
3032
vulnerabilities?: Bom['vulnerabilities']
3133
serialNumber?: Bom['serialNumber']
@@ -34,6 +36,7 @@ export interface OptionalBomProperties {
3436
export class Bom {
3537
metadata: Metadata
3638
components: ComponentRepository
39+
services: ServiceRepository
3740
vulnerabilities: VulnerabilityRepository
3841

3942
/** @see {@link version} */
@@ -54,6 +57,7 @@ export class Bom {
5457
constructor (op: OptionalBomProperties = {}) {
5558
this.metadata = op.metadata ?? new Metadata()
5659
this.components = op.components ?? new ComponentRepository()
60+
this.services= op.services?? new ServiceRepository()
5761
this.version = op.version ?? this.version
5862
this.vulnerabilities = op.vulnerabilities ?? new VulnerabilityRepository()
5963
this.serialNumber = op.serialNumber

src/models/component.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ export interface OptionalComponentProperties {
4949
supplier?: Component['supplier']
5050
swid?: Component['swid']
5151
version?: Component['version']
52-
dependencies?: Component['dependencies']
5352
components?: Component['components']
5453
cpe?: Component['cpe']
5554
properties?: Component['properties']
5655
evidence?: Component['evidence']
56+
57+
dependencies?: Component['dependencies']
5758
}
5859

5960
export class Component implements Comparable<Component> {
@@ -72,7 +73,6 @@ export class Component implements Comparable<Component> {
7273
supplier?: OrganizationalEntity
7374
swid?: SWID
7475
version?: string
75-
dependencies: BomRefRepository
7676
components: ComponentRepository
7777
properties: PropertyRepository
7878
evidence?: ComponentEvidence
@@ -83,6 +83,8 @@ export class Component implements Comparable<Component> {
8383
/** @see {@link cpe} */
8484
#cpe?: CPE
8585

86+
dependencies: BomRefRepository
87+
8688
/**
8789
* @throws {@link TypeError} if `op.cpe` is neither {@link CPE} nor `undefined`
8890
*/
@@ -103,11 +105,12 @@ export class Component implements Comparable<Component> {
103105
this.swid = op.swid
104106
this.version = op.version
105107
this.description = op.description
106-
this.dependencies = op.dependencies ?? new BomRefRepository()
107108
this.components = op.components ?? new ComponentRepository()
108109
this.cpe = op.cpe
109110
this.properties = op.properties ?? new PropertyRepository()
110111
this.evidence = op.evidence
112+
113+
this.dependencies = op.dependencies ?? new BomRefRepository()
111114
}
112115

113116
get bomRef (): BomRef {

src/models/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export * from './metadata'
3030
export * from './organizationalContact'
3131
export * from './organizationalEntity'
3232
export * from './property'
33+
export * from './service'
3334
export * from './swid'
3435
export * from './tool'
3536
export * as Vulnerability from './vulnerability'

src/models/service.ts

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
import type { Comparable } from "../_helpers/sortable";
21+
import { SortableComparables } from "../_helpers/sortable";
22+
import { treeIteratorSymbol } from "../_helpers/tree";
23+
import { BomRef, BomRefRepository } from "./bomRef";
24+
import { ExternalReferenceRepository } from "./externalReference";
25+
import { LicenseRepository } from "./license";
26+
import type {OrganizationalEntity } from "./organizationalEntity";
27+
import { PropertyRepository } from "./property";
28+
29+
export interface OptionalServiceProperties {
30+
bomRef?: BomRef['value']
31+
provider?: Service['provider']
32+
group?: Service['group']
33+
version?: Service['version']
34+
description?: Service['description']
35+
licenses?: Service['licenses']
36+
externalReferences?: Service['externalReferences']
37+
services?: Service['services']
38+
properties?: Service['properties']
39+
40+
dependencies?: Service['dependencies']
41+
}
42+
43+
export class Service implements Comparable<Service> {
44+
provider?: OrganizationalEntity
45+
group?: string
46+
name: string
47+
version?: string
48+
description?: string
49+
licenses: LicenseRepository
50+
externalReferences: ExternalReferenceRepository
51+
services: ServiceRepository
52+
properties: PropertyRepository
53+
54+
/** @see {@link bomRef} */
55+
readonly #bomRef: BomRef
56+
57+
dependencies: BomRefRepository
58+
59+
constructor(name: Service['name'], op: OptionalServiceProperties = {}) {
60+
this.#bomRef = new BomRef(op.bomRef)
61+
this.provider = op.provider
62+
this.group = op.group
63+
this.name = name
64+
this.version = op.version
65+
this.description = op.description
66+
this.licenses = op.licenses ?? new LicenseRepository()
67+
this.externalReferences = op.externalReferences ?? new ExternalReferenceRepository()
68+
this.services = op.services ?? new ServiceRepository()
69+
this.properties = op.properties ?? new PropertyRepository()
70+
71+
this.dependencies = op.dependencies ?? new BomRefRepository()
72+
}
73+
74+
get bomRef(): BomRef {
75+
return this.#bomRef
76+
}
77+
78+
compare(other: Service): number {
79+
// The purpose of this method is not to test for equality, but have deterministic comparability.
80+
const bomRefCompare = this.bomRef.compare(other.bomRef)
81+
if (bomRefCompare !== 0) {
82+
return bomRefCompare
83+
}
84+
/* eslint-disable @typescript-eslint/strict-boolean-expressions -- run compares in weighted order */
85+
return (this.group ?? '').localeCompare(other.group ?? '') ||
86+
this.name.localeCompare(other.name) ||
87+
(this.version ?? '').localeCompare(other.version ?? '')
88+
/* eslint-enable @typescript-eslint/strict-boolean-expressions */
89+
}
90+
91+
}
92+
93+
export class ServiceRepository extends SortableComparables<Service> {
94+
* [treeIteratorSymbol] (): Generator<Service> {
95+
for (const service of this) {
96+
yield service
97+
yield * service.services[treeIteratorSymbol]()
98+
}
99+
}
100+
}

src/serialize/baseSerializer.ts

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ export abstract class BaseSerializer<NormalizedBom> implements Serializer {
3636
}
3737
// endregion from components
3838

39+
// region from services
40+
for (const { bomRef } of bom.services[treeIteratorSymbol]()) {
41+
yield bomRef
42+
}
43+
// endregion from services
44+
3945
// region from vulnerabilities
4046
for (const { bomRef } of bom.vulnerabilities) {
4147
yield bomRef

src/serialize/json/normalize.ts

+48
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export class Factory {
5656
return new ComponentNormalizer(this)
5757
}
5858

59+
makeForService (): ServiceNormalizer {
60+
return new ServiceNormalizer(this)
61+
}
62+
5963
makeForComponentEvidence (): ComponentEvidenceNormalizer {
6064
return new ComponentEvidenceNormalizer(this)
6165
}
@@ -189,6 +193,9 @@ export class BomNormalizer extends BaseJsonNormalizer<Models.Bom> {
189193
? this._factory.makeForComponent().normalizeIterable(data.components, options)
190194
// spec < 1.4 requires `component` to be array
191195
: [],
196+
services: this._factory.spec.supportsServices && data.services.size > 0
197+
? this._factory.makeForService().normalizeIterable(data.services, options)
198+
: undefined,
192199
dependencies: this._factory.spec.supportsDependencyGraph
193200
? this._factory.makeForDependencyGraph().normalize(data, options)
194201
: undefined,
@@ -406,6 +413,44 @@ export class ComponentNormalizer extends BaseJsonNormalizer<Models.Component> {
406413
}
407414
}
408415

416+
export class ServiceNormalizer extends BaseJsonNormalizer<Models.Service> {
417+
normalize (data: Models.Service, options: NormalizerOptions): Normalized.Service {
418+
const spec = this._factory.spec
419+
return {
420+
'bom-ref': data.bomRef.value || undefined,
421+
provider: data.provider
422+
? this._factory.makeForOrganizationalEntity().normalize(data.provider, options)
423+
: undefined,
424+
group: data.group,
425+
name: data.name,
426+
version: data.version|| undefined,
427+
description: data.description || undefined,
428+
licenses: data.licenses.size > 0
429+
? this._factory.makeForLicense().normalizeIterable(data.licenses, options)
430+
: undefined,
431+
externalReferences: data.externalReferences.size > 0
432+
? this._factory.makeForExternalReference().normalizeIterable(data.externalReferences, options)
433+
: undefined,
434+
services: data.services.size > 0
435+
? this._factory.makeForService().normalizeIterable(data.services, options)
436+
: undefined,
437+
properties: spec.supportsProperties(data) && data.properties.size > 0
438+
? this._factory.makeForProperty().normalizeIterable(data.properties, options)
439+
: undefined,
440+
}
441+
}
442+
443+
normalizeIterable (data: SortableIterable<Models.Service>, options: NormalizerOptions): Normalized.Service[] {
444+
return (
445+
options.sortLists ?? false
446+
? data.sorted()
447+
: Array.from(data)
448+
).map(
449+
s => this.normalize(s, options)
450+
)
451+
}
452+
}
453+
409454
export class ComponentEvidenceNormalizer extends BaseJsonNormalizer<Models.ComponentEvidence> {
410455
normalize (data: Models.ComponentEvidence, options: NormalizerOptions): Normalized.ComponentEvidence {
411456
return {
@@ -599,6 +644,9 @@ export class DependencyGraphNormalizer extends BaseJsonNormalizer<Models.Bom> {
599644
for (const component of data.components[treeIteratorSymbol]()) {
600645
allRefs.set(component.bomRef, component.dependencies)
601646
}
647+
for (const service of data.services[treeIteratorSymbol]()) {
648+
allRefs.set(service.bomRef, service.dependencies)
649+
}
602650

603651
const normalized: Normalized.Dependency[] = []
604652
for (const [ref, deps] of allRefs) {

src/serialize/json/types.ts

+14
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export namespace Normalized {
8080
serialNumber?: string
8181
metadata?: Metadata
8282
components?: Component[]
83+
services?: Service[]
8384
externalReferences?: ExternalReference[]
8485
dependencies?: Dependency[]
8586
vulnerabilities?: Vulnerability[]
@@ -158,6 +159,19 @@ export namespace Normalized {
158159
properties?: Property[]
159160
}
160161

162+
export interface Service {
163+
'bom-ref'?: RefType
164+
provider?: OrganizationalEntity
165+
group?: string
166+
name: string
167+
version?: string
168+
description?: string
169+
licenses?: License[]
170+
externalReferences?: ExternalReference[]
171+
services?: Service[]
172+
properties?: Property[]
173+
}
174+
161175
export interface ComponentEvidence {
162176
licenses?: License[]
163177
copyright?: Copyright[]

0 commit comments

Comments
 (0)