From 1b3bdbf82189ef7f079c2611a56ad87dfe55d900 Mon Sep 17 00:00:00 2001 From: CAOShurong <170531907+CAOShurong@users.noreply.github.com> Date: Sun, 23 Aug 2026 03:34:28 +0800 Subject: [PATCH 1/2] feat(models): add address to OrganizationalEntity Adds optional address field (OrganizationalContact) to OrganizationalEntity and serialises it in JSON and XML output. Closes #1251 Signed-off-by: CAOShurong <170531907+CAOShurong@users.noreply.github.com> --- src/models/organizationalEntity.ts | 4 ++++ src/serialize/json/normalize.ts | 5 ++++- src/serialize/json/types.ts | 1 + src/serialize/xml/normalize.ts | 5 ++++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/models/organizationalEntity.ts b/src/models/organizationalEntity.ts index 262d8d842..89fc0fe78 100644 --- a/src/models/organizationalEntity.ts +++ b/src/models/organizationalEntity.ts @@ -19,23 +19,27 @@ Copyright (c) OWASP Foundation. All Rights Reserved. import type { Comparable } from '../_helpers/sortable' import { SortableComparables, SortableStringables } from '../_helpers/sortable' +import type { OrganizationalContact } from './organizationalContact' import { OrganizationalContactRepository } from './organizationalContact' 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?: OrganizationalContact 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 { diff --git a/src/serialize/json/normalize.ts b/src/serialize/json/normalize.ts index f76f57bd0..f02cddf1c 100644 --- a/src/serialize/json/normalize.ts +++ b/src/serialize/json/normalize.ts @@ -373,7 +373,10 @@ export class OrganizationalEntityNormalizer extends BaseJsonNormalizer 0 ? this._factory.makeForOrganizationalContact().normalizeIterable(data.contact, options) - : undefined + : undefined, + address: data.address === undefined + ? undefined + : this._factory.makeForOrganizationalContact().normalize(data.address, options) } } diff --git a/src/serialize/json/types.ts b/src/serialize/json/types.ts index 2fbf89490..6de86320b 100644 --- a/src/serialize/json/types.ts +++ b/src/serialize/json/types.ts @@ -136,6 +136,7 @@ export namespace Normalized { name?: string url?: JsonSchema.IriReference[] contact?: OrganizationalContact[] + address?: OrganizationalContact } export interface Hash { diff --git a/src/serialize/xml/normalize.ts b/src/serialize/xml/normalize.ts index a0ac955ad..9da7fa742 100644 --- a/src/serialize/xml/normalize.ts +++ b/src/serialize/xml/normalize.ts @@ -468,7 +468,10 @@ export class OrganizationalEntityNormalizer extends BaseXmlNormalizer escapeUri(s.toString()) ), options, 'url' ).filter(({ children: u }) => XmlSchema.isAnyURI(u)), - ...this._factory.makeForOrganizationalContact().normalizeIterable(data.contact, options, 'contact') + ...this._factory.makeForOrganizationalContact().normalizeIterable(data.contact, options, 'contact'), + ...(data.address === undefined + ? [] + : [this._factory.makeForOrganizationalContact().normalize(data.address, options, 'address')]) ].filter(isNotUndefined) } } From 357253ebb076397acb51d3cdf86ab9070ee17c0b Mon Sep 17 00:00:00 2001 From: Shurong Cao <170531907+CAOShurong@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:03:26 +0800 Subject: [PATCH 2/2] fix(models): use PostalAddress for organizational entity address Signed-off-by: Shurong Cao <170531907+CAOShurong@users.noreply.github.com> --- src/models/index.ts | 1 + src/models/organizationalEntity.ts | 7 +- src/models/postalAddress.ts | 70 +++++++++++++++++++ src/serialize/json/normalize.ts | 26 +++++-- src/serialize/json/types.ts | 11 ++- src/serialize/xml/normalize.ts | 32 +++++++-- tests/_data/models.js | 8 +++ .../json_sortedLists_spec1.6.json | 8 +++ .../json_sortedLists_spec1.7.json | 8 +++ .../xml_sortedLists_spec1.6.json | 36 ++++++++++ .../xml_sortedLists_spec1.7.json | 36 ++++++++++ .../json_complex_spec1.6.json.bin | 8 +++ .../json_complex_spec1.7.json.bin | 8 +++ .../xml_complex_spec1.6.xml.bin | 8 +++ .../xml_complex_spec1.7.xml.bin | 8 +++ 15 files changed, 264 insertions(+), 11 deletions(-) create mode 100644 src/models/postalAddress.ts 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 89fc0fe78..2d8b19685 100644 --- a/src/models/organizationalEntity.ts +++ b/src/models/organizationalEntity.ts @@ -19,8 +19,8 @@ Copyright (c) OWASP Foundation. All Rights Reserved. import type { Comparable } from '../_helpers/sortable' import { SortableComparables, SortableStringables } from '../_helpers/sortable' -import type { OrganizationalContact } from './organizationalContact' import { OrganizationalContactRepository } from './organizationalContact' +import type { PostalAddress } from './postalAddress' export interface OptionalOrganizationalEntityProperties { name?: OrganizationalEntity['name'] @@ -33,7 +33,7 @@ export class OrganizationalEntity implements Comparable { name?: string url: Set contact: OrganizationalContactRepository - address?: OrganizationalContact + address?: PostalAddress constructor (op: OptionalOrganizationalEntityProperties = {}) { this.name = op.name @@ -45,6 +45,9 @@ export class OrganizationalEntity implements Comparable { 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 f02cddf1c..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,23 +364,37 @@ 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, contact: data.contact.size > 0 ? this._factory.makeForOrganizationalContact().normalizeIterable(data.contact, options) - : undefined, - address: data.address === undefined - ? undefined - : this._factory.makeForOrganizationalContact().normalize(data.address, options) + : undefined } } diff --git a/src/serialize/json/types.ts b/src/serialize/json/types.ts index 6de86320b..59826cb10 100644 --- a/src/serialize/json/types.ts +++ b/src/serialize/json/types.ts @@ -132,11 +132,20 @@ 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[] - address?: OrganizationalContact } export interface Hash { diff --git a/src/serialize/xml/normalize.ts b/src/serialize/xml/normalize.ts index 9da7fa742..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,21 +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.address === undefined - ? [] - : [this._factory.makeForOrganizationalContact().normalize(data.address, options, 'address')]) + ...(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/