Skip to content

Commit 5fd9b44

Browse files
committed
feat: bom.vulnerabilities JSON normalization/serialization (CycloneDX#164)
Signed-off-by: Xavier Maso <[email protected]>
1 parent 1bb0aed commit 5fd9b44

File tree

10 files changed

+271
-13
lines changed

10 files changed

+271
-13
lines changed

src/models/vulnerability/reference.ts

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

20+
import type { Comparable } from '../../_helpers/sortable'
21+
import { SortableComparables } from '../../_helpers/sortable'
2022
import type { Source } from './source'
2123

2224
/**
@@ -27,7 +29,7 @@ import type { Source } from './source'
2729
*
2830
* @beta
2931
*/
30-
export class Reference {
32+
export class Reference implements Comparable<Reference> {
3133
/**
3234
* @example values
3335
* - `CVE-2021-39182`
@@ -41,7 +43,14 @@ export class Reference {
4143
this.id = id
4244
this.source = source
4345
}
46+
47+
compare (other: Reference): number {
48+
/* eslint-disable @typescript-eslint/strict-boolean-expressions -- run compares in weighted order */
49+
return (this.id ?? '').localeCompare(other.id ?? '') ||
50+
this.source.compare(other.source)
51+
/* eslint-enable @typescript-eslint/strict-boolean-expressions */
52+
}
4453
}
4554

46-
/** @beta */
47-
export class ReferenceRepository extends Set<Reference> {}
55+
export class ReferenceRepository extends SortableComparables<Reference> {
56+
}

src/models/vulnerability/source.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,31 @@ SPDX-License-Identifier: Apache-2.0
1717
Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

20+
import type { Comparable } from '../../_helpers/sortable'
21+
2022
/** @beta */
2123
export interface OptionalSourceProperties {
2224
name?: Source['name']
2325
url?: Source['url']
2426
}
2527

2628
/** @beta */
27-
export class Source {
29+
export class Source implements Comparable<Source> {
2830
name?: string
2931
url?: URL | string
3032

3133
constructor (op: OptionalSourceProperties = {}) {
3234
this.name = op.name
3335
this.url = op.url
3436
}
37+
38+
compare (other: Source): number {
39+
function normalizeUrl (u: URL | string): string {
40+
return (typeof u === 'string') ? u : u.toString()
41+
}
42+
const urlCompare = normalizeUrl(this.url ?? '').localeCompare(normalizeUrl(other.url ?? ''))
43+
44+
/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions -- run compares in weighted order */
45+
return (this.name ?? '').localeCompare(other.name ?? '') || urlCompare
46+
}
3547
}

src/models/vulnerability/vulnerability.ts

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

20+
import type { Comparable } from '../../_helpers/sortable'
21+
import { SortableComparables } from '../../_helpers/sortable'
2022
import { CweRepository } from '../../types'
2123
import { BomRef } from '../bomRef'
2224
import { PropertyRepository } from '../property'
@@ -27,7 +29,7 @@ import type { Analysis } from './analysis'
2729
import type { Credits } from './credits'
2830
import { RatingRepository } from './rating'
2931
import { ReferenceRepository } from './reference'
30-
import type { Source } from './source'
32+
import { Source } from './source'
3133

3234
/** @beta */
3335
export interface OptionalVulnerabilityProperties {
@@ -52,11 +54,11 @@ export interface OptionalVulnerabilityProperties {
5254
}
5355

5456
/** @beta */
55-
export class Vulnerability {
57+
export class Vulnerability implements Comparable<Vulnerability> {
5658
/** @see bomRef */
5759
readonly #bomRef: BomRef
5860
id?: string
59-
source?: Source
61+
source: Source
6062
references: ReferenceRepository
6163
ratings: RatingRepository
6264
cwes: CweRepository
@@ -76,7 +78,7 @@ export class Vulnerability {
7678
constructor (op: OptionalVulnerabilityProperties = {}) {
7779
this.#bomRef = new BomRef(op.bomRef)
7880
this.id = op.id
79-
this.source = op.source
81+
this.source = op.source ?? new Source()
8082
this.references = op.references ?? new ReferenceRepository()
8183
this.ratings = op.ratings ?? new RatingRepository()
8284
this.cwes = op.cwes ?? new CweRepository()
@@ -97,7 +99,20 @@ export class Vulnerability {
9799
get bomRef (): BomRef {
98100
return this.#bomRef
99101
}
102+
103+
compare (other: Vulnerability): number {
104+
const bomRefCompare = this.bomRef.compare(other.bomRef)
105+
if (bomRefCompare !== 0) {
106+
return bomRefCompare
107+
}
108+
/* eslint-disable @typescript-eslint/strict-boolean-expressions -- run compares in weighted order */
109+
return (this.id ?? '').localeCompare(other.id ?? '') ||
110+
this.source.compare(other.source) ||
111+
(this.description ?? '').localeCompare(other.description ?? '') ||
112+
(this.detail ?? '').localeCompare(other.detail ?? '')
113+
/* eslint-enable @typescript-eslint/strict-boolean-expressions */
114+
}
100115
}
101116

102117
/** @beta */
103-
export class VulnerabilityRepository extends Set<Vulnerability> {}
118+
export class VulnerabilityRepository extends SortableComparables<Vulnerability> {}

src/serialize/json/normalize.ts

+81
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,18 @@ export class Factory {
9090
makeForDependencyGraph (): DependencyGraphNormalizer {
9191
return new DependencyGraphNormalizer(this)
9292
}
93+
94+
makeForVulnerability (): VulnerabilityNormalizer {
95+
return new VulnerabilityNormalizer(this)
96+
}
97+
98+
makeForVulnerabilitySource (): VulnerabilitySourceNormalizer {
99+
return new VulnerabilitySourceNormalizer(this)
100+
}
101+
102+
makeForVulnerabilityReference (): VulnerabilityReferenceNormalizer {
103+
return new VulnerabilityReferenceNormalizer(this)
104+
}
93105
}
94106

95107
const schemaUrl: ReadonlyMap<SpecVersion, string> = new Map([
@@ -140,6 +152,9 @@ export class BomNormalizer extends BaseJsonNormalizer<Models.Bom> {
140152
: [],
141153
dependencies: this._factory.spec.supportsDependencyGraph
142154
? this._factory.makeForDependencyGraph().normalize(data, options)
155+
: undefined,
156+
vulnerabilities: this._factory.spec.supportsVulnerabilities && data.vulnerabilities.size > 0
157+
? this._factory.makeForVulnerability().normalizeIterable(data.vulnerabilities, options)
143158
: undefined
144159
}
145160
}
@@ -511,6 +526,72 @@ export class DependencyGraphNormalizer extends BaseJsonNormalizer<Models.Bom> {
511526
}
512527
}
513528

529+
export class VulnerabilityNormalizer extends BaseJsonNormalizer<Models.Vulnerability.Vulnerability> {
530+
normalize (data: Models.Vulnerability.Vulnerability, options: NormalizerOptions): Normalized.Vulnerability | undefined {
531+
const source = data.source !== undefined
532+
? this._factory.makeForVulnerabilitySource().normalize(data.source, options)
533+
: undefined
534+
const references = data.references.size > 0
535+
? this._factory.makeForVulnerabilityReference().normalizeIterable(data.references, options)
536+
: undefined
537+
538+
return {
539+
id: data.id,
540+
source,
541+
references,
542+
description: data.description,
543+
detail: data.detail,
544+
recommendation: data.recommendation,
545+
created: data.created,
546+
published: data.published,
547+
updated: data.updated
548+
}
549+
}
550+
551+
normalizeIterable (data: SortableIterable<Models.Vulnerability.Vulnerability>, options: NormalizerOptions): Normalized.Vulnerability[] {
552+
return (
553+
options.sortLists ?? false
554+
? data.sorted()
555+
: Array.from(data)
556+
).map(
557+
c => this.normalize(c, options)
558+
).filter(isNotUndefined)
559+
}
560+
}
561+
562+
export class VulnerabilitySourceNormalizer extends BaseJsonNormalizer<Models.Vulnerability.Source> {
563+
normalize (data: Models.Vulnerability.Source, options: NormalizerOptions): Normalized.VulnerabilitySource {
564+
const url = data.url !== undefined && typeof data.url !== 'string'
565+
? data.url.toString()
566+
: data.url
567+
return {
568+
name: data.name,
569+
url
570+
}
571+
}
572+
}
573+
574+
export class VulnerabilityReferenceNormalizer extends BaseJsonNormalizer<Models.Vulnerability.Reference> {
575+
normalize (data: Models.Vulnerability.Reference, options: NormalizerOptions): Normalized.VulnerabilityReference {
576+
return {
577+
id: data.id,
578+
source: data.source !== undefined
579+
? this._factory.makeForVulnerabilitySource().normalize(data.source, options)
580+
: undefined
581+
}
582+
}
583+
584+
normalizeIterable (data: SortableIterable<Models.Vulnerability.Reference>, options: NormalizerOptions): Normalized.VulnerabilityReference[] {
585+
return (
586+
options.sortLists ?? false
587+
? data.sorted()
588+
: Array.from(data)
589+
).map(
590+
c => this.normalize(c, options)
591+
).filter(isNotUndefined)
592+
}
593+
}
594+
514595
/* eslint-enable @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/strict-boolean-expressions */
515596

516597
function normalizeStringableIter (data: Iterable<Stringable>, options: NormalizerOptions): string[] {

src/serialize/json/types.ts

+23
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export namespace Normalized {
7474
components?: Component[]
7575
externalReferences?: ExternalReference[]
7676
dependencies?: Dependency[]
77+
vulnerabilities?: Vulnerability[]
7778
}
7879

7980
export interface Metadata {
@@ -190,4 +191,26 @@ export namespace Normalized {
190191
dependsOn?: RefType[]
191192
}
192193

194+
export interface Vulnerability {
195+
id?: string
196+
source?: VulnerabilitySource
197+
references?: VulnerabilityReference[]
198+
description?: string
199+
detail?: string
200+
recommendation?: string
201+
created?: Date
202+
published?: Date
203+
updated?: Date
204+
205+
}
206+
207+
export interface VulnerabilitySource {
208+
name?: string
209+
url?: string
210+
}
211+
212+
export interface VulnerabilityReference {
213+
id?: string
214+
source?: VulnerabilitySource
215+
}
193216
}

src/spec.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface Protocol {
4747
supportsToolReferences: boolean
4848
requiresComponentVersion: boolean
4949
supportsProperties: (model: any) => boolean
50+
supportsVulnerabilities: boolean
5051
}
5152

5253
/**
@@ -64,6 +65,7 @@ class Spec implements Protocol {
6465
readonly #supportsToolReferences: boolean
6566
readonly #requiresComponentVersion: boolean
6667
readonly #supportsProperties: boolean
68+
readonly #supportsVulnerabilities: boolean
6769

6870
constructor (
6971
version: Version,
@@ -75,7 +77,8 @@ class Spec implements Protocol {
7577
supportsDependencyGraph: boolean,
7678
supportsToolReferences: boolean,
7779
requiresComponentVersion: boolean,
78-
supportsProperties: boolean
80+
supportsProperties: boolean,
81+
supportsVulnerabilities: boolean
7982
) {
8083
this.#version = version
8184
this.#formats = new Set(formats)
@@ -87,6 +90,7 @@ class Spec implements Protocol {
8790
this.#supportsToolReferences = supportsToolReferences
8891
this.#requiresComponentVersion = requiresComponentVersion
8992
this.#supportsProperties = supportsProperties
93+
this.#supportsVulnerabilities = supportsVulnerabilities
9094
}
9195

9296
get version (): Version {
@@ -130,6 +134,10 @@ class Spec implements Protocol {
130134
// currently a global allow/deny -- might work based on input, in the future
131135
return this.#supportsProperties
132136
}
137+
138+
get supportsVulnerabilities (): boolean {
139+
return this.#supportsVulnerabilities
140+
}
133141
}
134142

135143
/** Specification v1.2 */
@@ -184,6 +192,7 @@ export const Spec1dot2: Readonly<Protocol> = Object.freeze(new Spec(
184192
true,
185193
false,
186194
true,
195+
false,
187196
false
188197
))
189198

@@ -239,7 +248,8 @@ export const Spec1dot3: Readonly<Protocol> = Object.freeze(new Spec(
239248
true,
240249
false,
241250
true,
242-
true
251+
true,
252+
false
243253
))
244254

245255
/** Specification v1.4 */
@@ -295,6 +305,7 @@ export const Spec1dot4: Readonly<Protocol> = Object.freeze(new Spec(
295305
true,
296306
true,
297307
false,
308+
true,
298309
true
299310
))
300311

tests/_data/models.js

+29
Original file line numberDiff line numberDiff line change
@@ -197,5 +197,34 @@ module.exports.createComplexStructure = function () {
197197
])
198198
}))
199199

200+
bom.vulnerabilities.add(new Models.Vulnerability.Vulnerability({
201+
id: '1',
202+
source: new Models.Vulnerability.Source({ name: 'manual' }),
203+
references: new Models.Vulnerability.ReferenceRepository([
204+
new Models.Vulnerability.Reference('CVE-2042-42420'),
205+
new Models.Vulnerability.Reference('CVE-2042-42421')
206+
]),
207+
description: 'description of 1',
208+
detail: 'detail of 1',
209+
recommendation: 'recommendation of 1',
210+
created: '2023-03-03T00:00:00.040Z',
211+
published: '2023-03-03T00:00:00.041Z',
212+
updated: '2023-03-03T00:00:00.042Z'
213+
}))
214+
215+
bom.vulnerabilities.add(new Models.Vulnerability.Vulnerability({
216+
id: '2',
217+
source: new Models.Vulnerability.Source({ name: 'manual' }),
218+
references: new Models.Vulnerability.ReferenceRepository([
219+
new Models.Vulnerability.Reference('CVE-2042-42422')
220+
]),
221+
description: 'description of 2',
222+
detail: 'detail of 2',
223+
recommendation: 'recommendation of 2',
224+
created: '2023-03-03T00:00:00.040Z',
225+
published: '2023-03-03T00:00:00.041Z',
226+
updated: '2023-03-03T00:00:00.042Z'
227+
}))
228+
200229
return bom
201230
}

0 commit comments

Comments
 (0)