Skip to content

Commit

Permalink
feat: tools as Components & Services (#1163)
Browse files Browse the repository at this point in the history
* 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 <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck authored Nov 16, 2024
1 parent ccdd7f5 commit 5ff1d20
Show file tree
Hide file tree
Showing 44 changed files with 1,711 additions and 460 deletions.
14 changes: 14 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ All notable changes to this project will be documented in this file.

<!-- add unreleased items here -->

* 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])

[#1152]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/1152
[#1163]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1163

## 6.12.0 -- 2024-11-12

* Added
Expand Down
26 changes: 26 additions & 0 deletions src/_helpers/iterable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*!
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 function * chainI<T> (...iterables: Array<Iterable<T>>): Generator<T> {
for (const iterable of iterables) {
for (const item of iterable) {
yield item
}
}
}
6 changes: 3 additions & 3 deletions src/models/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { LifecycleRepository } from './lifecycle'
import { OrganizationalContactRepository } from './organizationalContact'
import type { OrganizationalEntity } from './organizationalEntity'
import { PropertyRepository } from './property'
import { ToolRepository } from './tool'
import { Tools } from './tool'

export interface OptionalMetadataProperties {
timestamp?: Metadata['timestamp']
Expand All @@ -40,7 +40,7 @@ export interface OptionalMetadataProperties {
export class Metadata {
timestamp?: Date
lifecycles: LifecycleRepository
tools: ToolRepository
tools: Tools
authors: OrganizationalContactRepository
component?: Component
manufacture?: OrganizationalEntity
Expand All @@ -51,7 +51,7 @@ export class Metadata {
constructor (op: OptionalMetadataProperties = {}) {
this.timestamp = op.timestamp
this.lifecycles = op.lifecycles ?? new LifecycleRepository()
this.tools = op.tools ?? new ToolRepository()
this.tools = op.tools ?? new Tools()
this.authors = op.authors ?? new OrganizationalContactRepository()
this.component = op.component
this.manufacture = op.manufacture
Expand Down
48 changes: 48 additions & 0 deletions src/models/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ Copyright (c) OWASP Foundation. All Rights Reserved.

import type { Comparable } from '../_helpers/sortable'
import { SortableComparables } from '../_helpers/sortable'
import type { Component } from "./component";
import { ComponentRepository} from "./component";
import { ExternalReferenceRepository } from './externalReference'
import { HashDictionary } from './hash'
import type { Service } from "./service";
import { ServiceRepository } from "./service";

export interface OptionalToolProperties {
vendor?: Tool['vendor']
Expand Down Expand Up @@ -53,7 +57,51 @@ export class Tool implements Comparable<Tool> {
(this.version ?? '').localeCompare(other.version ?? '')
/* eslint-enable @typescript-eslint/strict-boolean-expressions */
}

static fromComponent(component: Component): Tool {
return new Tool({
vendor: component.group,
name: component.name,
version: component.version,
hashes: component.hashes,
externalReferences: component.externalReferences
})
}

static fromService(service: Service): Tool {
return new Tool({
vendor: service.group,
name: service.name,
version: service.version,
externalReferences: service.externalReferences
})
}
}

export class ToolRepository extends SortableComparables<Tool> {
}


export interface OptionalToolsProperties {
components?: Tools['components']
services?: Tools['services']
tools?: Tools['tools']
}

export class Tools {
components: ComponentRepository
services: ServiceRepository
tools: ToolRepository

constructor(op: OptionalToolsProperties = {}) {
this.components = op.components ?? new ComponentRepository()
this.services = op.services ?? new ServiceRepository()
this.tools = op.tools ?? new ToolRepository()
}

get size(): number {
return this.components.size
+ this.services.size
+ this.tools.size
}
}
6 changes: 3 additions & 3 deletions src/models/vulnerability/vulnerability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { SortableComparables } from '../../_helpers/sortable'
import { CweRepository } from '../../types/cwe'
import { BomRef } from '../bomRef'
import { PropertyRepository } from '../property'
import { ToolRepository } from '../tool'
import { Tools } from '../tool'
import { AdvisoryRepository } from './advisory'
import { AffectRepository } from './affect'
import type { Analysis } from './analysis'
Expand Down Expand Up @@ -68,7 +68,7 @@ export class Vulnerability implements Comparable<Vulnerability> {
published?: Date
updated?: Date
credits?: Credits
tools: ToolRepository
tools: Tools
analysis?: Analysis
affects: AffectRepository
properties: PropertyRepository
Expand All @@ -88,7 +88,7 @@ export class Vulnerability implements Comparable<Vulnerability> {
this.published = op.published
this.updated = op.updated
this.credits = op.credits
this.tools = op.tools ?? new ToolRepository()
this.tools = op.tools ?? new Tools()
this.analysis = op.analysis
this.affects = op.affects ?? new AffectRepository()
this.properties = op.properties ?? new PropertyRepository()
Expand Down
27 changes: 25 additions & 2 deletions src/serialize/json/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ SPDX-License-Identifier: Apache-2.0
Copyright (c) OWASP Foundation. All Rights Reserved.
*/

import { chainI } from "../../_helpers/iterable";
import { isNotUndefined } from '../../_helpers/notUndefined'
import type { SortableIterable } from '../../_helpers/sortable'
import type { Stringable } from '../../_helpers/stringable'
Expand All @@ -25,6 +26,7 @@ import { escapeUri } from '../../_helpers/uri'
import type * as Models from '../../models'
import { LicenseExpression, NamedLicense, SpdxLicense } from '../../models/license'
import { NamedLifecycle } from '../../models/lifecycle'
import { Tool, ToolRepository } from '../../models/tool'
import { AffectedSingleVersion, AffectedVersionRange } from '../../models/vulnerability/affect'
import { isSupportedSpdxId } from '../../spdx'
import type { _SpecProtocol as Spec } from '../../spec/_protocol'
Expand Down Expand Up @@ -72,6 +74,10 @@ export class Factory {
return new ToolNormalizer(this)
}

makeForTools (): ToolsNormalizer {
return new ToolsNormalizer(this)
}

makeForOrganizationalContact (): OrganizationalContactNormalizer {
return new OrganizationalContactNormalizer(this)
}
Expand Down Expand Up @@ -221,7 +227,7 @@ export class MetadataNormalizer extends BaseJsonNormalizer<Models.Metadata> {
? this._factory.makeForLifecycle().normalizeIterable(data.lifecycles, options)
: undefined,
tools: data.tools.size > 0
? this._factory.makeForTool().normalizeIterable(data.tools, options)
? this._factory.makeForTools().normalize(data.tools, options)
: undefined,
authors: data.authors.size > 0
? this._factory.makeForOrganizationalContact().normalizeIterable(data.authors, options)
Expand Down Expand Up @@ -285,6 +291,23 @@ export class ToolNormalizer extends BaseJsonNormalizer<Models.Tool> {
}
}

export class ToolsNormalizer extends BaseJsonNormalizer<Models.Tools> {
normalize(data: Models.Tools, options: NormalizerOptions): Normalized.ToolsType {
if (data.tools.size > 0 || !this._factory.spec.supportsToolsComponentsServices) {
return this._factory.makeForTool().normalizeIterable(
new ToolRepository(chainI<Models.Tool>(
Array.from(data.components, Tool.fromComponent),
Array.from(data.services, Tool.fromService),
data.tools,
)), options)
}
return {
components: this._factory.makeForComponent().normalizeIterable(data.components, options),
services: this._factory.makeForService().normalizeIterable(data.services, options)
}
}
}

export class HashNormalizer extends BaseJsonNormalizer<Models.Hash> {
normalize ([algorithm, content]: Models.Hash, options: NormalizerOptions): Normalized.Hash | undefined {
const spec = this._factory.spec
Expand Down Expand Up @@ -723,7 +746,7 @@ export class VulnerabilityNormalizer extends BaseJsonNormalizer<Models.Vulnerabi
? undefined
: this._factory.makeForVulnerabilityCredits().normalize(data.credits, options),
tools: data.tools.size > 0
? this._factory.makeForTool().normalizeIterable(data.tools, options)
? this._factory.makeForTools().normalize(data.tools, options)
: undefined,
analysis: data.analysis === undefined
? undefined
Expand Down
12 changes: 10 additions & 2 deletions src/serialize/json/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export namespace Normalized {
export interface Metadata {
timestamp?: JsonSchema.DateTime
lifecycles?: Lifecycle[]
tools?: Tool[]
tools?: ToolsType
authors?: OrganizationalContact[]
component?: Component
manufacture?: OrganizationalEntity
Expand Down Expand Up @@ -118,6 +118,14 @@ export namespace Normalized {
externalReferences?: ExternalReference[]
}

/** since CDX 1.5 */
export interface Tools {
components: Component[]
services: Service[]
}

export type ToolsType = Tools | Tool[]

export interface OrganizationalContact {
name?: string
email?: JsonSchema.IdnEmail
Expand Down Expand Up @@ -257,7 +265,7 @@ export namespace Normalized {
published?: JsonSchema.DateTime
updated?: JsonSchema.DateTime
credits?: Vulnerability.Credits
tools?: Tool[]
tools?: ToolsType
analysis?: Vulnerability.Analysis
affects?: Vulnerability.Affect[]
properties?: Property[]
Expand Down
52 changes: 42 additions & 10 deletions src/serialize/xml/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ SPDX-License-Identifier: Apache-2.0
Copyright (c) OWASP Foundation. All Rights Reserved.
*/

import { chainI } from "../../_helpers/iterable";
import { isNotUndefined } from '../../_helpers/notUndefined'
import type { SortableIterable } from '../../_helpers/sortable'
import type { Stringable } from '../../_helpers/stringable'
import { treeIteratorSymbol } from '../../_helpers/tree'
import { escapeUri } from '../../_helpers/uri'
import type * as Models from '../../models'
import { Tool, ToolRepository } from "../../models";
import { LicenseExpression, NamedLicense, SpdxLicense } from '../../models/license'
import { NamedLifecycle } from '../../models/lifecycle'
import { AffectedSingleVersion, AffectedVersionRange } from '../../models/vulnerability/affect'
Expand Down Expand Up @@ -75,6 +77,10 @@ export class Factory {
return new ToolNormalizer(this)
}

makeForTools (): ToolsNormalizer {
return new ToolsNormalizer(this)
}

makeForOrganizationalContact (): OrganizationalContactNormalizer {
return new OrganizationalContactNormalizer(this)
}
Expand Down Expand Up @@ -250,11 +256,7 @@ export class MetadataNormalizer extends BaseXmlNormalizer<Models.Metadata> {
}
: undefined
const tools: SimpleXml.Element | undefined = data.tools.size > 0
? {
type: 'element',
name: 'tools',
children: this._factory.makeForTool().normalizeIterable(data.tools, options, 'tool')
}
? this._factory.makeForTools().normalize(data.tools, options, 'tools')
: undefined
const authors: SimpleXml.Element | undefined = data.authors.size > 0
? {
Expand Down Expand Up @@ -369,6 +371,40 @@ export class ToolNormalizer extends BaseXmlNormalizer<Models.Tool> {
}
}

export class ToolsNormalizer extends BaseXmlNormalizer<Models.Tools> {
normalize (data: Models.Tools, options: NormalizerOptions, elementName: string): SimpleXml.Element {
let children: SimpleXml.Element[] = []
if (data.tools.size > 0 || !this._factory.spec.supportsToolsComponentsServices) {
children = this._factory.makeForTool().normalizeIterable(
new ToolRepository(chainI(
Array.from(data.components, Tool.fromComponent),
Array.from(data.services, Tool.fromService),
data.tools,
)), options, 'tool')
} else {
if (data.components.size > 0) {
children.push({
type: 'element',
name: 'components',
children: this._factory.makeForComponent().normalizeIterable(data.components, options, 'component')
})
}
if (data.components.size > 0) {
children.push({
type: 'element',
name: 'services',
children: this._factory.makeForService().normalizeIterable(data.services, options, 'service')
})
}
}
return {
type: 'element',
name: elementName,
children
}
}
}

export class HashNormalizer extends BaseXmlNormalizer<Models.Hash> {
normalize ([algorithm, content]: Models.Hash, options: NormalizerOptions, elementName: string): SimpleXml.Element | undefined {
const spec = this._factory.spec
Expand Down Expand Up @@ -935,11 +971,7 @@ export class VulnerabilityNormalizer extends BaseXmlNormalizer<Models.Vulnerabil
}
: undefined
const tools: SimpleXml.Element | undefined = data.tools.size > 0
? {
type: 'element',
name: 'tools',
children: this._factory.makeForTool().normalizeIterable(data.tools, options, 'tool')
}
? this._factory.makeForTools().normalize(data.tools, options, 'tools')
: undefined
const affects: SimpleXml.Element | undefined = data.affects.size > 0
? {
Expand Down
Loading

0 comments on commit 5ff1d20

Please sign in to comment.