diff --git a/src/models/index.ts b/src/models/index.ts index d55989e73..ba0900ada 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -30,6 +30,7 @@ export * from './lifecycle' export * from './metadata' export * from './organizationalContact' export * from './organizationalEntity' +export * from './postalAddress' export * from './property' export * from './service' export * from './swid' diff --git a/src/models/organizationalEntity.ts b/src/models/organizationalEntity.ts index 262d8d842..2d8b19685 100644 --- a/src/models/organizationalEntity.ts +++ b/src/models/organizationalEntity.ts @@ -20,27 +20,34 @@ Copyright (c) OWASP Foundation. All Rights Reserved. import type { Comparable } from '../_helpers/sortable' import { SortableComparables, SortableStringables } from '../_helpers/sortable' import { OrganizationalContactRepository } from './organizationalContact' +import type { PostalAddress } from './postalAddress' export interface OptionalOrganizationalEntityProperties { name?: OrganizationalEntity['name'] url?: OrganizationalEntity['url'] contact?: OrganizationalEntity['contact'] + address?: OrganizationalEntity['address'] } export class OrganizationalEntity implements Comparable { name?: string url: Set contact: OrganizationalContactRepository + address?: PostalAddress constructor (op: OptionalOrganizationalEntityProperties = {}) { this.name = op.name this.url = op.url ?? new Set() this.contact = op.contact ?? new OrganizationalContactRepository() + this.address = op.address } compare (other: OrganizationalEntity): number { /* eslint-disable @typescript-eslint/strict-boolean-expressions -- run compares in weighted order */ return (this.name ?? '').localeCompare(other.name ?? '') || + (this.address === undefined + ? other.address === undefined ? 0 : -1 + : other.address === undefined ? 1 : this.address.compare(other.address)) || this.contact.compare(other.contact) || (new SortableStringables(this.url)).compare(new SortableStringables(other.url)) /* eslint-enable @typescript-eslint/strict-boolean-expressions */ diff --git a/src/models/postalAddress.ts b/src/models/postalAddress.ts new file mode 100644 index 000000000..098041af5 --- /dev/null +++ b/src/models/postalAddress.ts @@ -0,0 +1,70 @@ +/*! +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. +*/ + +import type { Comparable } from '../_helpers/sortable' + +export interface OptionalPostalAddressProperties { + country?: PostalAddress['country'] + region?: PostalAddress['region'] + locality?: PostalAddress['locality'] + postOfficeBoxNumber?: PostalAddress['postOfficeBoxNumber'] + postalCode?: PostalAddress['postalCode'] + streetAddress?: PostalAddress['streetAddress'] +} + +/** + * A postal address used to identify a contactable location. + * + * @see https://cyclonedx.org/docs/1.7/xml/#type_postalAddressType + */ +export class PostalAddress implements Comparable { + country?: string + region?: string + locality?: string + postOfficeBoxNumber?: string + postalCode?: string + streetAddress?: string + + constructor (op: OptionalPostalAddressProperties = {}) { + this.country = op.country + this.region = op.region + this.locality = op.locality + this.postOfficeBoxNumber = op.postOfficeBoxNumber + this.postalCode = op.postalCode + this.streetAddress = op.streetAddress + } + + compare (other: PostalAddress): number { + const comparables: Array<[string | undefined, string | undefined]> = [ + [this.country, other.country], + [this.region, other.region], + [this.locality, other.locality], + [this.postOfficeBoxNumber, other.postOfficeBoxNumber], + [this.postalCode, other.postalCode], + [this.streetAddress, other.streetAddress] + ] + for (const [selfValue, otherValue] of comparables) { + const compared = (selfValue ?? '').localeCompare(otherValue ?? '') + if (compared !== 0) { + return compared + } + } + return 0 + } +} diff --git a/src/serialize/json/normalize.ts b/src/serialize/json/normalize.ts index f76f57bd0..77f34a4d9 100644 --- a/src/serialize/json/normalize.ts +++ b/src/serialize/json/normalize.ts @@ -87,6 +87,10 @@ export class Factory { return new OrganizationalContactNormalizer(this) } + makeForPostalAddress (): PostalAddressNormalizer { + return new PostalAddressNormalizer(this) + } + makeForOrganizationalEntity (): OrganizationalEntityNormalizer { return new OrganizationalEntityNormalizer(this) } @@ -360,14 +364,31 @@ export class OrganizationalContactNormalizer extends BaseJsonNormalizer { + normalize (data: Models.PostalAddress, options: NormalizerOptions): Normalized.PostalAddress { + return { + country: data.country || undefined, + region: data.region || undefined, + locality: data.locality || undefined, + postOfficeBoxNumber: data.postOfficeBoxNumber || undefined, + postalCode: data.postalCode || undefined, + streetAddress: data.streetAddress || undefined + } + } +} + export class OrganizationalEntityNormalizer extends BaseJsonNormalizer { normalize (data: Models.OrganizationalEntity, options: NormalizerOptions): Normalized.OrganizationalEntity { const urls = normalizeStringableIter( Array.from(data.url, (s) => escapeUri(s.toString())), options ).filter(JsonSchema.isIriReference) + const address = data.address === undefined || this._factory.spec.version < SpecVersion.v1dot6 + ? undefined + : this._factory.makeForPostalAddress().normalize(data.address, options) return { name: data.name || undefined, + address, url: urls.length > 0 ? urls : undefined, diff --git a/src/serialize/json/types.ts b/src/serialize/json/types.ts index 2fbf89490..59826cb10 100644 --- a/src/serialize/json/types.ts +++ b/src/serialize/json/types.ts @@ -132,8 +132,18 @@ export namespace Normalized { phone?: string } + export interface PostalAddress { + country?: string + region?: string + locality?: string + postOfficeBoxNumber?: string + postalCode?: string + streetAddress?: string + } + export interface OrganizationalEntity { name?: string + address?: PostalAddress url?: JsonSchema.IriReference[] contact?: OrganizationalContact[] } diff --git a/src/serialize/xml/normalize.ts b/src/serialize/xml/normalize.ts index a0ac955ad..146a83e76 100644 --- a/src/serialize/xml/normalize.ts +++ b/src/serialize/xml/normalize.ts @@ -90,6 +90,10 @@ export class Factory { return new OrganizationalContactNormalizer(this) } + makeForPostalAddress (): PostalAddressNormalizer { + return new PostalAddressNormalizer(this) + } + makeForOrganizationalEntity (): OrganizationalEntityNormalizer { return new OrganizationalEntityNormalizer(this) } @@ -457,18 +461,41 @@ export class OrganizationalContactNormalizer extends BaseXmlNormalizer { + normalize (data: Models.PostalAddress, options: NormalizerOptions, elementName: string): SimpleXml.Element { + return { + type: 'element', + name: elementName, + children: [ + makeOptionalTextElement(data.country, 'country'), + makeOptionalTextElement(data.region, 'region'), + makeOptionalTextElement(data.locality, 'locality'), + makeOptionalTextElement(data.postOfficeBoxNumber, 'postOfficeBoxNumber'), + makeOptionalTextElement(data.postalCode, 'postalCode'), + makeOptionalTextElement(data.streetAddress, 'streetAddress') + ].filter(isNotUndefined) + } + } +} + export class OrganizationalEntityNormalizer extends BaseXmlNormalizer { normalize (data: Models.OrganizationalEntity, options: NormalizerOptions, elementName: string): SimpleXml.Element { + const address = data.address === undefined || this._factory.spec.version < SpecVersion.v1dot6 + ? undefined + : this._factory.makeForPostalAddress().normalize(data.address, options, 'address') return { type: 'element', name: elementName, children: [ makeOptionalTextElement(data.name, 'name', normalizedString), + address, ...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') + ...(data.contact.size > 0 + ? this._factory.makeForOrganizationalContact().normalizeIterable(data.contact, options, 'contact') + : []) ].filter(isNotUndefined) } } diff --git a/tests/_data/models.js b/tests/_data/models.js index 86962aff0..29f25d59c 100644 --- a/tests/_data/models.js +++ b/tests/_data/models.js @@ -89,6 +89,14 @@ function createComplexStructure () { }), manufacture: new Models.OrganizationalEntity({ name: 'meta manufacture', + address: new Models.PostalAddress({ + country: 'United States', + region: 'Texas', + locality: 'Austin', + postOfficeBoxNumber: '901', + postalCode: '78758', + streetAddress: '100 Main Street' + }), url: new Set([new URL('https://meta-manufacture.xmpl')]) }), supplier: new Models.OrganizationalEntity({ diff --git a/tests/_data/normalizeResults/json_sortedLists_spec1.6.json b/tests/_data/normalizeResults/json_sortedLists_spec1.6.json index 5d3face98..728f652bb 100644 --- a/tests/_data/normalizeResults/json_sortedLists_spec1.6.json +++ b/tests/_data/normalizeResults/json_sortedLists_spec1.6.json @@ -79,6 +79,14 @@ }, "manufacture": { "name": "meta manufacture", + "address": { + "country": "United States", + "region": "Texas", + "locality": "Austin", + "postOfficeBoxNumber": "901", + "postalCode": "78758", + "streetAddress": "100 Main Street" + }, "url": [ "https://meta-manufacture.xmpl/" ] diff --git a/tests/_data/normalizeResults/json_sortedLists_spec1.7.json b/tests/_data/normalizeResults/json_sortedLists_spec1.7.json index ba49c0fba..05ff7f0fd 100644 --- a/tests/_data/normalizeResults/json_sortedLists_spec1.7.json +++ b/tests/_data/normalizeResults/json_sortedLists_spec1.7.json @@ -79,6 +79,14 @@ }, "manufacture": { "name": "meta manufacture", + "address": { + "country": "United States", + "region": "Texas", + "locality": "Austin", + "postOfficeBoxNumber": "901", + "postalCode": "78758", + "streetAddress": "100 Main Street" + }, "url": [ "https://meta-manufacture.xmpl/" ] diff --git a/tests/_data/normalizeResults/xml_sortedLists_spec1.6.json b/tests/_data/normalizeResults/xml_sortedLists_spec1.6.json index 6385a0eeb..817be3457 100644 --- a/tests/_data/normalizeResults/xml_sortedLists_spec1.6.json +++ b/tests/_data/normalizeResults/xml_sortedLists_spec1.6.json @@ -271,6 +271,42 @@ "name": "name", "children": "meta manufacture" }, + { + "type": "element", + "name": "address", + "children": [ + { + "type": "element", + "name": "country", + "children": "United States" + }, + { + "type": "element", + "name": "region", + "children": "Texas" + }, + { + "type": "element", + "name": "locality", + "children": "Austin" + }, + { + "type": "element", + "name": "postOfficeBoxNumber", + "children": "901" + }, + { + "type": "element", + "name": "postalCode", + "children": "78758" + }, + { + "type": "element", + "name": "streetAddress", + "children": "100 Main Street" + } + ] + }, { "type": "element", "name": "url", diff --git a/tests/_data/normalizeResults/xml_sortedLists_spec1.7.json b/tests/_data/normalizeResults/xml_sortedLists_spec1.7.json index 339a3e936..6267e0ed0 100644 --- a/tests/_data/normalizeResults/xml_sortedLists_spec1.7.json +++ b/tests/_data/normalizeResults/xml_sortedLists_spec1.7.json @@ -271,6 +271,42 @@ "name": "name", "children": "meta manufacture" }, + { + "type": "element", + "name": "address", + "children": [ + { + "type": "element", + "name": "country", + "children": "United States" + }, + { + "type": "element", + "name": "region", + "children": "Texas" + }, + { + "type": "element", + "name": "locality", + "children": "Austin" + }, + { + "type": "element", + "name": "postOfficeBoxNumber", + "children": "901" + }, + { + "type": "element", + "name": "postalCode", + "children": "78758" + }, + { + "type": "element", + "name": "streetAddress", + "children": "100 Main Street" + } + ] + }, { "type": "element", "name": "url", diff --git a/tests/_data/serializeResults/json_complex_spec1.6.json.bin b/tests/_data/serializeResults/json_complex_spec1.6.json.bin index fce78fef1..1c3d81a1f 100644 --- a/tests/_data/serializeResults/json_complex_spec1.6.json.bin +++ b/tests/_data/serializeResults/json_complex_spec1.6.json.bin @@ -79,6 +79,14 @@ }, "manufacture": { "name": "meta manufacture", + "address": { + "country": "United States", + "region": "Texas", + "locality": "Austin", + "postOfficeBoxNumber": "901", + "postalCode": "78758", + "streetAddress": "100 Main Street" + }, "url": [ "https://meta-manufacture.xmpl/" ] diff --git a/tests/_data/serializeResults/json_complex_spec1.7.json.bin b/tests/_data/serializeResults/json_complex_spec1.7.json.bin index 2b5571adf..7a7e65f6f 100644 --- a/tests/_data/serializeResults/json_complex_spec1.7.json.bin +++ b/tests/_data/serializeResults/json_complex_spec1.7.json.bin @@ -79,6 +79,14 @@ }, "manufacture": { "name": "meta manufacture", + "address": { + "country": "United States", + "region": "Texas", + "locality": "Austin", + "postOfficeBoxNumber": "901", + "postalCode": "78758", + "streetAddress": "100 Main Street" + }, "url": [ "https://meta-manufacture.xmpl/" ] diff --git a/tests/_data/serializeResults/xml_complex_spec1.6.xml.bin b/tests/_data/serializeResults/xml_complex_spec1.6.xml.bin index 4c633acb4..7552eeea3 100644 --- a/tests/_data/serializeResults/xml_complex_spec1.6.xml.bin +++ b/tests/_data/serializeResults/xml_complex_spec1.6.xml.bin @@ -63,6 +63,14 @@ meta manufacture +
+ United States + Texas + Austin + 901 + 78758 + 100 Main Street +
https://meta-manufacture.xmpl/
diff --git a/tests/_data/serializeResults/xml_complex_spec1.7.xml.bin b/tests/_data/serializeResults/xml_complex_spec1.7.xml.bin index b4865d0cd..7bb668b33 100644 --- a/tests/_data/serializeResults/xml_complex_spec1.7.xml.bin +++ b/tests/_data/serializeResults/xml_complex_spec1.7.xml.bin @@ -63,6 +63,14 @@ meta manufacture +
+ United States + Texas + Austin + 901 + 78758 + 100 Main Street +
https://meta-manufacture.xmpl/