UNPKG

@cyclonedx/cyclonedx-library

Version:

Core functionality of CycloneDX for JavaScript (Node.js or WebBrowser).

1,293 lines (1,181 loc) 46.6 kB
/*! 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. */ /* eslint complexity: ["error", 50 ] -- acknowledged */ /* eslint max-lines: 'off' -- intended */ 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' import { isSupportedSpdxId } from '../../spdx' import type { _SpecProtocol as Spec } from '../../spec/_protocol' import { Version as SpecVersion } from '../../spec/enums' import type { NormalizerOptions } from '../types' import { normalizedString, token} from './_xsd' import type { SimpleXml } from './types' import { XmlSchema } from './types' export class Factory { readonly #spec: Spec constructor (spec: Factory['spec']) { this.#spec = spec } get spec (): Spec { return this.#spec } makeForBom (): BomNormalizer { return new BomNormalizer(this) } makeForMetadata (): MetadataNormalizer { return new MetadataNormalizer(this) } makeForLifecycle (): LifecycleNormalizer { return new LifecycleNormalizer(this) } makeForComponent (): ComponentNormalizer { return new ComponentNormalizer(this) } makeForService (): ServiceNormalizer { return new ServiceNormalizer(this) } makeForComponentEvidence (): ComponentEvidenceNormalizer { return new ComponentEvidenceNormalizer(this) } makeForTool (): ToolNormalizer { return new ToolNormalizer(this) } makeForTools (): ToolsNormalizer { return new ToolsNormalizer(this) } makeForOrganizationalContact (): OrganizationalContactNormalizer { return new OrganizationalContactNormalizer(this) } makeForOrganizationalEntity (): OrganizationalEntityNormalizer { return new OrganizationalEntityNormalizer(this) } makeForHash (): HashNormalizer { return new HashNormalizer(this) } makeForLicense (): LicenseNormalizer { return new LicenseNormalizer(this) } makeForSWID (): SWIDNormalizer { return new SWIDNormalizer(this) } makeForExternalReference (): ExternalReferenceNormalizer { return new ExternalReferenceNormalizer(this) } makeForAttachment (): AttachmentNormalizer { return new AttachmentNormalizer(this) } makeForProperty (): PropertyNormalizer { return new PropertyNormalizer(this) } makeForDependencyGraph (): DependencyGraphNormalizer { return new DependencyGraphNormalizer(this) } makeForVulnerability (): VulnerabilityNormalizer { return new VulnerabilityNormalizer(this) } makeForVulnerabilitySource (): VulnerabilitySourceNormalizer { return new VulnerabilitySourceNormalizer(this) } makeForVulnerabilityReference (): VulnerabilityReferenceNormalizer { return new VulnerabilityReferenceNormalizer(this) } makeForVulnerabilityRating (): VulnerabilityRatingNormalizer { return new VulnerabilityRatingNormalizer(this) } makeForVulnerabilityAdvisory (): VulnerabilityAdvisoryNormalizer { return new VulnerabilityAdvisoryNormalizer(this) } makeForVulnerabilityCredits (): VulnerabilityCreditsNormalizer { return new VulnerabilityCreditsNormalizer(this) } makeForVulnerabilityAffect (): VulnerabilityAffectNormalizer { return new VulnerabilityAffectNormalizer(this) } makeForVulnerabilityAffectedVersion (): VulnerabilityAffectedVersionNormalizer { return new VulnerabilityAffectedVersionNormalizer(this) } makeForVulnerabilityAnalysis (): VulnerabilityAnalysisNormalizer { return new VulnerabilityAnalysisNormalizer(this) } } const xmlNamespace: ReadonlyMap<SpecVersion, string> = new Map([ [SpecVersion.v1dot6, 'http://cyclonedx.org/schema/bom/1.6'], [SpecVersion.v1dot5, 'http://cyclonedx.org/schema/bom/1.5'], [SpecVersion.v1dot4, 'http://cyclonedx.org/schema/bom/1.4'], [SpecVersion.v1dot3, 'http://cyclonedx.org/schema/bom/1.3'], [SpecVersion.v1dot2, 'http://cyclonedx.org/schema/bom/1.2'], [SpecVersion.v1dot1, 'http://cyclonedx.org/schema/bom/1.1'], [SpecVersion.v1dot0, 'http://cyclonedx.org/schema/bom/1.0'] ]) interface XmlNormalizer<TModel, TNormalized> { normalize: (data: TModel, options: NormalizerOptions, elementName?: string) => TNormalized | undefined normalizeIterable?: (data: SortableIterable<TModel>, options: NormalizerOptions, elementName: string) => TNormalized[] } abstract class BaseXmlNormalizer<TModel, TNormalized = SimpleXml.Element> implements XmlNormalizer<TModel, TNormalized> { protected readonly _factory: Factory constructor (factory: Factory) { this._factory = factory } get factory (): Factory { return this._factory } /** * @param elementName - element name. XML defines structures; the element's name is defined on usage of a structure. */ abstract normalize (data: TModel, options: NormalizerOptions, elementName?: string): TNormalized | undefined } /* eslint-disable @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/strict-boolean-expressions -- * since empty strings need to be treated as undefined/null */ export class BomNormalizer extends BaseXmlNormalizer<Models.Bom> { normalize (data: Models.Bom, options: NormalizerOptions): SimpleXml.Element { const components: SimpleXml.Element = { // spec < 1.4 always requires a 'components' element type: 'element', name: 'components', children: data.components.size > 0 ? this._factory.makeForComponent().normalizeIterable(data.components, options, 'component') : undefined } const services: SimpleXml.Element | undefined = this._factory.spec.supportsServices && data.services.size > 0 ? { type: 'element', name: 'services', children: this._factory.makeForService().normalizeIterable(data.services, options, 'service') } : undefined const vulnerabilities: SimpleXml.Element | undefined = this._factory.spec.supportsVulnerabilities && data.vulnerabilities.size > 0 ? { type: 'element', name: 'vulnerabilities', children: this._factory.makeForVulnerability().normalizeIterable(data.vulnerabilities, options, 'vulnerability') } : undefined return { type: 'element', // the element's name is hardcoded in the XSD name: 'bom', namespace: xmlNamespace.get(this._factory.spec.version), attributes: { version: data.version, serialNumber: this.#isEligibleSerialNumber(data.serialNumber) ? data.serialNumber : undefined }, children: [ this._factory.makeForMetadata().normalize(data.metadata, options, 'metadata'), components, services, this._factory.spec.supportsDependencyGraph ? this._factory.makeForDependencyGraph().normalize(data, options, 'dependencies') : undefined, vulnerabilities ].filter(isNotUndefined) } } #isEligibleSerialNumber (v: string | undefined): boolean { return v !== undefined && // see https://github.com/CycloneDX/specification/blob/ef71717ae0ecb564c0b4c9536d6e9e57e35f2e69/schema/bom-1.4.xsd#L699 /^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/.test(v) } } export class MetadataNormalizer extends BaseXmlNormalizer<Models.Metadata> { normalize (data: Models.Metadata, options: NormalizerOptions, elementName: string): SimpleXml.Element { const orgEntityNormalizer = this._factory.makeForOrganizationalEntity() const lifecycles: SimpleXml.Element | undefined = this._factory.spec.supportsMetadataLifecycles && data.lifecycles.size > 0 ? { type: 'element', name: 'lifecycles', children: this._factory.makeForLifecycle().normalizeIterable(data.lifecycles, options, 'lifecycle') } : undefined const tools: SimpleXml.Element | undefined = data.tools.size > 0 ? this._factory.makeForTools().normalize(data.tools, options, 'tools') : undefined const authors: SimpleXml.Element | undefined = data.authors.size > 0 ? { type: 'element', name: 'authors', children: this._factory.makeForOrganizationalContact().normalizeIterable(data.authors, options, 'author') } : undefined const licenses: SimpleXml.Element | undefined = this._factory.spec.supportsMetadataLicenses && data.licenses.size > 0 ? { type: 'element', name: 'licenses', children: this._factory.makeForLicense().normalizeIterable(data.licenses, options) } : undefined const properties: SimpleXml.Element | undefined = this._factory.spec.supportsMetadataProperties && data.properties.size > 0 ? { type: 'element', name: 'properties', children: this._factory.makeForProperty().normalizeIterable(data.properties, options, 'property') } : undefined return { type: 'element', name: elementName, children: [ makeOptionalDateTimeElement(data.timestamp, 'timestamp'), lifecycles, tools, authors, data.component === undefined ? undefined : this._factory.makeForComponent().normalize(data.component, options, 'component'), data.manufacture === undefined ? undefined : orgEntityNormalizer.normalize(data.manufacture, options, 'manufacture'), data.supplier === undefined ? undefined : orgEntityNormalizer.normalize(data.supplier, options, 'supplier'), licenses, properties ].filter(isNotUndefined) } } } export class LifecycleNormalizer extends BaseXmlNormalizer<Models.Lifecycle> { normalize (data: Models.Lifecycle, options: NormalizerOptions, elementName: string): SimpleXml.Element { return data instanceof NamedLifecycle ? { type: 'element', name: elementName, children: [ makeTextElement(data.name, 'name', normalizedString), makeOptionalTextElement(data.description, 'description') ].filter(isNotUndefined) } : { type: 'element', name: elementName, children: [ makeTextElement(data, 'phase') ] } } normalizeIterable (data: SortableIterable<Models.Lifecycle>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(t => this.normalize(t, options, elementName)) } } export class ToolNormalizer extends BaseXmlNormalizer<Models.Tool> { normalize (data: Models.Tool, options: NormalizerOptions, elementName: string): SimpleXml.Element { const hashes: SimpleXml.Element | undefined = data.hashes.size > 0 ? { type: 'element', name: 'hashes', children: this._factory.makeForHash().normalizeIterable(data.hashes, options, 'hash') } : undefined const externalReferences: SimpleXml.Element | undefined = this._factory.spec.supportsToolReferences && data.externalReferences.size > 0 ? { type: 'element', name: 'externalReferences', children: this._factory.makeForExternalReference().normalizeIterable(data.externalReferences, options, 'reference') } : undefined return { type: 'element', name: elementName, children: [ makeOptionalTextElement(data.vendor, 'vendor', normalizedString), makeOptionalTextElement(data.name, 'name', normalizedString), makeOptionalTextElement(data.version, 'version', normalizedString), hashes, externalReferences ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.Tool>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(t => this.normalize(t, options, elementName)) } } 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.services.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 return spec.supportsHashAlgorithm(algorithm) && spec.supportsHashValue(content) ? { type: 'element', name: elementName, attributes: { alg: algorithm }, children: token(content) } : undefined } normalizeIterable (data: SortableIterable<Models.Hash>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map( h => this.normalize(h, options, elementName) ).filter(isNotUndefined) } } export class OrganizationalContactNormalizer extends BaseXmlNormalizer<Models.OrganizationalContact> { normalize (data: Models.OrganizationalContact, options: NormalizerOptions, elementName: string): SimpleXml.Element { return { type: 'element', name: elementName, children: [ makeOptionalTextElement(data.name, 'name', normalizedString), makeOptionalTextElement(data.email, 'email', normalizedString), makeOptionalTextElement(data.phone, 'phone', normalizedString) ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.OrganizationalContact>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(oc => this.normalize(oc, options, elementName)) } } export class OrganizationalEntityNormalizer extends BaseXmlNormalizer<Models.OrganizationalEntity> { normalize (data: Models.OrganizationalEntity, options: NormalizerOptions, elementName: string): SimpleXml.Element { return { type: 'element', name: elementName, children: [ makeOptionalTextElement(data.name, 'name', normalizedString), ...makeTextElementIter(Array.from( data.url, (s): string => escapeUri(s.toString()) ), options, 'url' ).filter(({ children: u }) => XmlSchema.isAnyURI(u)), ...this._factory.makeForOrganizationalContact().normalizeIterable(data.contact, options, 'contact') ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.OrganizationalEntity>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(oe => this.normalize(oe, options, elementName)) } } export class ComponentNormalizer extends BaseXmlNormalizer<Models.Component> { normalize (data: Models.Component, options: NormalizerOptions, elementName: string): SimpleXml.Element | undefined { const spec = this._factory.spec if (!spec.supportsComponentType(data.type)) { return undefined } const supplier: SimpleXml.Element | undefined = data.supplier === undefined ? undefined : this._factory.makeForOrganizationalEntity().normalize(data.supplier, options, 'supplier') const version: SimpleXml.Element | undefined = ( spec.requiresComponentVersion ? makeTextElement : makeOptionalTextElement )( data.version ?? '', 'version', normalizedString ) const hashes: SimpleXml.Element | undefined = data.hashes.size > 0 ? { type: 'element', name: 'hashes', children: this._factory.makeForHash().normalizeIterable(data.hashes, options, 'hash') } : undefined const licenses: SimpleXml.Element | undefined = data.licenses.size > 0 ? { type: 'element', name: 'licenses', children: this._factory.makeForLicense().normalizeIterable(data.licenses, options) } : undefined const swid: SimpleXml.Element | undefined = data.swid === undefined ? undefined : this._factory.makeForSWID().normalize(data.swid, options, 'swid') const extRefs: SimpleXml.Element | undefined = data.externalReferences.size > 0 ? { type: 'element', name: 'externalReferences', children: this._factory.makeForExternalReference().normalizeIterable(data.externalReferences, options, 'reference') } : undefined const properties: SimpleXml.Element | undefined = spec.supportsProperties(data) && data.properties.size > 0 ? { type: 'element', name: 'properties', children: this._factory.makeForProperty().normalizeIterable(data.properties, options, 'property') } : undefined const components: SimpleXml.Element | undefined = data.components.size > 0 ? { type: 'element', name: 'components', children: this.normalizeIterable(data.components, options, 'component') } : undefined const evidence: SimpleXml.Element | undefined = spec.supportsComponentEvidence && data.evidence !== undefined ? this._factory.makeForComponentEvidence().normalize(data.evidence, options, 'evidence') : undefined return { type: 'element', name: elementName, attributes: { type: data.type, 'bom-ref': data.bomRef.value }, children: [ supplier, makeOptionalTextElement(data.author, 'author', normalizedString), makeOptionalTextElement(data.publisher, 'publisher', normalizedString), makeOptionalTextElement(data.group, 'group', normalizedString), makeTextElement(data.name, 'name', normalizedString), version, makeOptionalTextElement(data.description, 'description', normalizedString), makeOptionalTextElement(data.scope, 'scope'), hashes, licenses, makeOptionalTextElement(data.copyright, 'copyright', normalizedString), makeOptionalTextElement(data.cpe, 'cpe'), makeOptionalTextElement(data.purl, 'purl'), swid, extRefs, properties, components, evidence ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.Component>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map( c => this.normalize(c, options, elementName) ).filter(isNotUndefined) } } export class ServiceNormalizer extends BaseXmlNormalizer<Models.Service> { normalize (data: Models.Service, options: NormalizerOptions, elementName: string): SimpleXml.Element { const spec = this._factory.spec const provider: SimpleXml.Element | undefined = data.provider === undefined ? undefined : this._factory.makeForOrganizationalEntity().normalize(data.provider, options, 'provider') const licenses: SimpleXml.Element | undefined = data.licenses.size > 0 ? { type: 'element', name: 'licenses', children: this._factory.makeForLicense().normalizeIterable(data.licenses, options) } : undefined const extRefs: SimpleXml.Element | undefined = data.externalReferences.size > 0 ? { type: 'element', name: 'externalReferences', children: this._factory.makeForExternalReference().normalizeIterable(data.externalReferences, options, 'reference') } : undefined const properties: SimpleXml.Element | undefined = spec.supportsProperties(data) && data.properties.size > 0 ? { type: 'element', name: 'properties', children: this._factory.makeForProperty().normalizeIterable(data.properties, options, 'property') } : undefined const services: SimpleXml.Element | undefined = data.services.size > 0 ? { type: 'element', name: 'services', children: this.normalizeIterable(data.services, options, 'service') } : undefined return { type: 'element', name: elementName, attributes: { 'bom-ref': data.bomRef.value }, children: [ provider, makeOptionalTextElement(data.group, 'group', normalizedString), makeTextElement(data.name, 'name', normalizedString), makeOptionalTextElement(data.version, 'version', normalizedString), makeOptionalTextElement(data.description, 'description', normalizedString), licenses, extRefs, properties, services, ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.Service>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map( s => this.normalize(s, options, elementName) ) } } export class ComponentEvidenceNormalizer extends BaseXmlNormalizer<Models.ComponentEvidence> { normalize (data: Models.ComponentEvidence, options: NormalizerOptions, elementName: string): SimpleXml.Element { const licenses: SimpleXml.Element | undefined = data.licenses.size > 0 ? { type: 'element', name: 'licenses', children: this._factory.makeForLicense().normalizeIterable(data.licenses, options) } : undefined const copyright: SimpleXml.Element | undefined = data.copyright.size > 0 ? { type: 'element', name: 'copyright', children: makeTextElementIter(data.copyright, options, 'text') } : undefined return { type: 'element', name: elementName, children: [ licenses, copyright ].filter(isNotUndefined) } } } export class LicenseNormalizer extends BaseXmlNormalizer<Models.License> { normalize (data: Models.License, options: NormalizerOptions): SimpleXml.Element { switch (true) { case data instanceof NamedLicense: return this.#normalizeNamedLicense(data, options) case data instanceof SpdxLicense: return isSupportedSpdxId(data.id) ? this.#normalizeSpdxLicense(data, options) : this.#normalizeNamedLicense(new NamedLicense( // prevent information loss -> convert to broader type data.id, { url: data.url } ), options) case data instanceof LicenseExpression: return this.#normalizeLicenseExpression(data) /* c8 ignore start */ default: // this case is expected to never happen - and therefore is undocumented throw new TypeError('Unexpected LicenseChoice') /* c8 ignore end */ } } #normalizeNamedLicense (data: Models.NamedLicense, options: NormalizerOptions): SimpleXml.Element { const url = escapeUri(data.url?.toString()) return { type: 'element', name: 'license', attributes: { acknowledgement: this._factory.spec.supportsLicenseAcknowledgement ? data.acknowledgement : undefined }, children: [ makeTextElement(data.name, 'name', normalizedString), data.text === undefined ? undefined : this._factory.makeForAttachment().normalize(data.text, options, 'text'), XmlSchema.isAnyURI(url) ? makeTextElement(url, 'url') : undefined ].filter(isNotUndefined) } } #normalizeSpdxLicense (data: Models.SpdxLicense, options: NormalizerOptions): SimpleXml.Element { const url = escapeUri(data.url?.toString()) return { type: 'element', name: 'license', attributes: { acknowledgement: this._factory.spec.supportsLicenseAcknowledgement ? data.acknowledgement : undefined }, children: [ makeTextElement(data.id, 'id'), data.text === undefined ? undefined : this._factory.makeForAttachment().normalize(data.text, options, 'text'), XmlSchema.isAnyURI(url) ? makeTextElement(url, 'url') : undefined ].filter(isNotUndefined) } } #normalizeLicenseExpression (data: Models.LicenseExpression): SimpleXml.Element { const elem = makeTextElement(data.expression, 'expression', normalizedString) elem.attributes = { acknowledgement: this._factory.spec.supportsLicenseAcknowledgement ? data.acknowledgement : undefined } return elem } /** * If there is any {@link Models.LicenseExpression | LicenseExpression} in the set, then this is the only item that is normalized. */ normalizeIterable (data: SortableIterable<Models.License>, options: NormalizerOptions): SimpleXml.Element[] { const licenses = options.sortLists ?? false ? data.sorted() : Array.from(data) if (licenses.length > 1) { const expressions = licenses.filter(l => l instanceof LicenseExpression) if (expressions.length > 0) { // could have thrown {@link RangeError} when there is more than one only {@link Models.LicenseExpression | LicenseExpression}. // but let's be graceful and just normalize to the most relevant choice: any expression return [this.#normalizeLicenseExpression(expressions[0])] } } return licenses.map(l => this.normalize(l, options)) } } export class SWIDNormalizer extends BaseXmlNormalizer<Models.SWID> { normalize (data: Models.SWID, options: NormalizerOptions, elementName: string): SimpleXml.Element { const url = escapeUri(data.url?.toString()) return { type: 'element', name: elementName, attributes: { tagId: data.tagId, name: data.name, version: data.version || undefined, tagVersion: data.tagVersion, patch: data.patch === undefined ? undefined : (data.patch ? 'true' : 'false') }, children: [ data.text === undefined ? undefined : this._factory.makeForAttachment().normalize(data.text, options, 'text'), XmlSchema.isAnyURI(url) ? makeTextElement(url, 'url') : undefined ].filter(isNotUndefined) } } } export class ExternalReferenceNormalizer extends BaseXmlNormalizer<Models.ExternalReference> { normalize (data: Models.ExternalReference, options: NormalizerOptions, elementName: string): SimpleXml.Element | undefined { const url = escapeUri(data.url.toString()) const hashes: SimpleXml.Element | undefined = this._factory.spec.supportsExternalReferenceHashes && data.hashes.size > 0 ? { type: 'element', name: 'hashes', children: this._factory.makeForHash().normalizeIterable(data.hashes, options, 'hash') } : undefined return this._factory.spec.supportsExternalReferenceType(data.type) && XmlSchema.isAnyURI(url) ? { type: 'element', name: elementName, attributes: { type: data.type }, children: [ makeTextElement(url, 'url'), makeOptionalTextElement(data.comment, 'comment'), hashes ].filter(isNotUndefined) } : undefined } normalizeIterable (data: SortableIterable<Models.ExternalReference>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map( r => this.normalize(r, options, elementName) ).filter(isNotUndefined) } } export class AttachmentNormalizer extends BaseXmlNormalizer<Models.Attachment> { normalize (data: Models.Attachment, options: NormalizerOptions, elementName: string): SimpleXml.Element { return { type: 'element', name: elementName, attributes: { 'content-type': data.contentType ? normalizedString(data.contentType) : undefined, encoding: data.encoding || undefined }, children: data.content.toString() } } } export class PropertyNormalizer extends BaseXmlNormalizer<Models.Property> { normalize (data: Models.Property, options: NormalizerOptions, elementName: string): SimpleXml.Element { return { type: 'element', name: elementName, attributes: { name: data.name }, children: normalizedString(data.value) } } normalizeIterable (data: SortableIterable<Models.Property>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(p => this.normalize(p, options, elementName)) } } export class DependencyGraphNormalizer extends BaseXmlNormalizer<Models.Bom> { normalize (data: Models.Bom, options: NormalizerOptions, elementName: string): SimpleXml.Element | undefined { 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[treeIteratorSymbol]()) { allRefs.set(component.bomRef, component.dependencies) } } for (const component of data.components[treeIteratorSymbol]()) { allRefs.set(component.bomRef, component.dependencies) } for (const service of data.services[treeIteratorSymbol]()) { allRefs.set(service.bomRef, service.dependencies) } const normalized: Array<(SimpleXml.Element & { attributes: { ref: string } })> = [] for (const [ref, deps] of allRefs) { const dep = this.#normalizeDependency(ref, deps, allRefs, options) if (isNotUndefined(dep)) { normalized.push(dep) } } if (options.sortLists ?? false) { normalized.sort( ({ attributes: { ref: a } }, { attributes: { ref: b } }) => a.localeCompare(b)) } return { type: 'element', name: elementName, children: normalized } } #normalizeDependency ( ref: Models.BomRef, deps: Models.BomRefRepository, allRefs: Map<Models.BomRef, Models.BomRefRepository>, options: NormalizerOptions ): undefined | (SimpleXml.Element & { attributes: { ref: string } }) { const bomRef = ref.toString() if (bomRef.length === 0) { // no value -> cannot render return undefined } const dependsOn: string[] = Array.from(deps).filter(d => allRefs.has(d) && d !== ref) .map(d => d.toString()).filter(d => d.length > 0) if (options.sortLists ?? false) { dependsOn.sort((a, b) => a.localeCompare(b)) } return { type: 'element', name: 'dependency', attributes: { ref: bomRef }, children: dependsOn.map(d => ({ type: 'element', name: 'dependency', attributes: { ref: d } })) } } } export class VulnerabilityNormalizer extends BaseXmlNormalizer<Models.Vulnerability.Vulnerability> { normalize (data: Models.Vulnerability.Vulnerability, options: NormalizerOptions, elementName: string): SimpleXml.Element { const references: SimpleXml.Element | undefined = data.references.size > 0 ? { type: 'element', name: 'references', children: this._factory.makeForVulnerabilityReference().normalizeIterable(data.references, options, 'reference') } : undefined const ratings: SimpleXml.Element | undefined = data.ratings.size > 0 ? { type: 'element', name: 'ratings', children: this._factory.makeForVulnerabilityRating().normalizeIterable(data.ratings, options, 'rating') } : undefined const cwes: SimpleXml.Element | undefined = data.cwes.size > 0 ? { type: 'element', name: 'cwes', children: ( options.sortLists ? data.cwes.sorted() : Array.from(data.cwes) ).map(cwe => makeTextElement(cwe, 'cwe')) } : undefined const advisories: SimpleXml.Element | undefined = data.advisories.size > 0 ? { type: 'element', name: 'advisories', children: this._factory.makeForVulnerabilityAdvisory().normalizeIterable(data.advisories, options, 'advisory') } : undefined const tools: SimpleXml.Element | undefined = data.tools.size > 0 ? this._factory.makeForTools().normalize(data.tools, options, 'tools') : undefined const affects: SimpleXml.Element | undefined = data.affects.size > 0 ? { type: 'element', name: 'affects', children: this._factory.makeForVulnerabilityAffect().normalizeIterable(data.affects, options, 'target') } : undefined const properties: SimpleXml.Element | undefined = data.properties.size > 0 ? { type: 'element', name: 'properties', children: this._factory.makeForProperty().normalizeIterable(data.properties, options, 'property') } : undefined return { type: 'element', name: elementName, attributes: { 'bom-ref': data.bomRef.value || undefined }, children: [ makeOptionalTextElement(data.id, 'id', normalizedString), data.source === undefined ? undefined : this._factory.makeForVulnerabilitySource().normalize(data.source, options, 'source'), references, ratings, cwes, makeOptionalTextElement(data.description, 'description'), makeOptionalTextElement(data.detail, 'detail'), makeOptionalTextElement(data.recommendation, 'recommendation'), advisories, makeOptionalDateTimeElement(data.created, 'created'), makeOptionalDateTimeElement(data.created, 'published'), makeOptionalDateTimeElement(data.created, 'updated'), data.credits === undefined ? undefined : this._factory.makeForVulnerabilityCredits().normalize(data.credits, options, 'credits'), tools, data.analysis === undefined ? undefined : this._factory.makeForVulnerabilityAnalysis().normalize(data.analysis, options, 'analysis'), affects, properties ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.Vulnerability.Vulnerability>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(v => this.normalize(v, options, elementName)) } } export class VulnerabilitySourceNormalizer extends BaseXmlNormalizer<Models.Vulnerability.Source> { normalize (data: Models.Vulnerability.Source, options: NormalizerOptions, elementName: string): SimpleXml.Element { const url = escapeUri(data.url?.toString()) return { type: 'element', name: elementName, children: [ makeOptionalTextElement(data.name, 'name', normalizedString), XmlSchema.isAnyURI(url) ? makeTextElement(url, 'url') : undefined ].filter(isNotUndefined) } } } export class VulnerabilityReferenceNormalizer extends BaseXmlNormalizer<Models.Vulnerability.Reference> { normalize (data: Models.Vulnerability.Reference, options: NormalizerOptions, elementName: string): SimpleXml.Element { return { type: 'element', name: elementName, children: [ makeTextElement(data.id, 'id'), this._factory.makeForVulnerabilitySource().normalize(data.source, options, 'source') ] } } normalizeIterable (data: SortableIterable<Models.Vulnerability.Reference>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(r => this.normalize(r, options, elementName)) } } export class VulnerabilityRatingNormalizer extends BaseXmlNormalizer<Models.Vulnerability.Rating> { normalize (data: Models.Vulnerability.Rating, options: NormalizerOptions, elementName: string): SimpleXml.Element { return { type: 'element', name: elementName, children: [ data.source === undefined ? undefined : this._factory.makeForVulnerabilitySource().normalize(data.source, options, 'source'), makeOptionalTextElement(data.score, 'score'), makeOptionalTextElement(data.severity, 'severity'), this._factory.spec.supportsVulnerabilityRatingMethod(data.method) ? makeOptionalTextElement(data.method, 'method') : undefined, makeOptionalTextElement(data.vector, 'vector', normalizedString), makeOptionalTextElement(data.justification, 'justification') ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.Vulnerability.Rating>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(r => this.normalize(r, options, elementName)) } } export class VulnerabilityAdvisoryNormalizer extends BaseXmlNormalizer<Models.Vulnerability.Advisory> { normalize (data: Models.Vulnerability.Advisory, options: NormalizerOptions, elementName: string): SimpleXml.Element | undefined { const url = escapeUri(data.url.toString()) if (!XmlSchema.isAnyURI(url)) { // invalid value -> cannot render return undefined } return { type: 'element', name: elementName, children: [ makeOptionalTextElement(data.title, 'title'), makeTextElement(url, 'url') ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.Vulnerability.Advisory>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map( a => this.normalize(a, options, elementName) ).filter(isNotUndefined) } } export class VulnerabilityCreditsNormalizer extends BaseXmlNormalizer<Models.Vulnerability.Credits> { normalize (data: Models.Vulnerability.Credits, options: NormalizerOptions, elementName: string): SimpleXml.Element { const organizations: SimpleXml.Element | undefined = data.organizations.size > 0 ? { type: 'element', name: 'organizations', children: this._factory.makeForOrganizationalEntity().normalizeIterable(data.organizations, options, 'organization') } : undefined const individuals: SimpleXml.Element | undefined = data.individuals.size > 0 ? { type: 'element', name: 'individuals', children: this._factory.makeForOrganizationalContact().normalizeIterable(data.individuals, options, 'individual') } : undefined return { type: 'element', name: elementName, children: [ organizations, individuals ].filter(isNotUndefined) } } } export class VulnerabilityAnalysisNormalizer extends BaseXmlNormalizer<Models.Vulnerability.Analysis> { normalize (data: Models.Vulnerability.Analysis, options: NormalizerOptions, elementName: string): SimpleXml.Element { const responses: SimpleXml.Element | undefined = data.response.size > 0 ? { type: 'element', name: 'responses', children: ( options.sortLists ?? false ? data.response.sorted() : Array.from(data.response) ).map(ar => makeTextElement(ar, 'response')) } : undefined return { type: 'element', name: elementName, children: [ makeOptionalTextElement(data.state, 'state'), makeOptionalTextElement(data.justification, 'justification'), responses, makeOptionalTextElement(data.detail, 'detail') ].filter(isNotUndefined) } } } export class VulnerabilityAffectNormalizer extends BaseXmlNormalizer<Models.Vulnerability.Affect> { normalize (data: Models.Vulnerability.Affect, options: NormalizerOptions, elementName: string): SimpleXml.Element { const versions: SimpleXml.Element | undefined = data.versions.size > 0 ? { type: 'element', name: 'versions', children: this._factory.makeForVulnerabilityAffectedVersion().normalizeIterable(data.versions, options, 'version') } : undefined return { type: 'element', name: elementName, children: [ makeTextElement(data.ref, 'ref'), versions ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.Vulnerability.Affect>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(a => this.normalize(a, options, elementName)) } } export class VulnerabilityAffectedVersionNormalizer extends BaseXmlNormalizer<Models.Vulnerability.AffectedVersion> { normalize (data: Models.Vulnerability.AffectedVersion, options: NormalizerOptions, elementName: string): SimpleXml.Element { switch (true) { case data instanceof AffectedSingleVersion: return this.#normalizeAffectedSingleVersion(data, elementName) case data instanceof AffectedVersionRange: return this.#normalizeAffectedVersionRange(data, elementName) /* c8 ignore start */ default: // this case is expected to never happen - and therefore is undocumented throw new TypeError('Unexpected Vulnerability AffectedVersion') /* c8 ignore end */ } } #normalizeAffectedSingleVersion (data: Models.Vulnerability.AffectedSingleVersion, elementName: string): SimpleXml.Element { return { type: 'element', name: elementName, children: [ makeTextElement(data.version, 'version', normalizedString), makeOptionalTextElement(data.status, 'status') ].filter(isNotUndefined) } } #normalizeAffectedVersionRange (data: Models.Vulnerability.AffectedVersionRange, elementName: string): SimpleXml.Element { return { type: 'element', name: elementName, children: [ makeTextElement(data.range, 'range', normalizedString), makeOptionalTextElement(data.status, 'status') ].filter(isNotUndefined) } } normalizeIterable (data: SortableIterable<Models.Vulnerability.AffectedVersion>, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { return ( options.sortLists ?? false ? data.sorted() : Array.from(data) ).map(av => this.normalize(av, options, elementName)) } } /* eslint-enable @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/strict-boolean-expressions */ type StrictTextElement = SimpleXml.TextElement & { children: string } type TextElementModifier = (i:string) => string const noTEM: TextElementModifier = (s) => s function makeOptionalTextElement (data: null | undefined | Stringable, elementName: string, mod: TextElementModifier = noTEM): undefined | StrictTextElement { const s = mod(data?.toString() ?? '') return s.length > 0 ? makeTextElement(s, elementName) : undefined } function makeTextElement (data: Stringable, elementName: string, mod: TextElementModifier = noTEM): StrictTextElement { return { type: 'element', name: elementName, children: mod(data.toString()) } } function makeTextElementIter (data: Iterable<Stringable>, options: NormalizerOptions, elementName: string, mod: TextElementModifier = noTEM): StrictTextElement[] { const r: StrictTextElement[] = Array.from(data, d => makeTextElement(d, elementName, mod)) if (options.sortLists ?? false) { r.sort(({ children: a }, { children: b }) => a.localeCompare(b)) } return r } function makeOptionalDateTimeElement (data: null | undefined | Date, elementName: string, mod: TextElementModifier = noTEM): undefined | StrictTextElement { const d = data?.toISOString() return d === undefined ? undefined : makeTextElement(d, elementName, mod) }