diff --git a/src/models/license.ts b/src/models/license.ts index 6fd83b7ce..03a9c86d3 100644 --- a/src/models/license.ts +++ b/src/models/license.ts @@ -21,6 +21,7 @@ import type { Sortable } from '../_helpers/sortable' import type { LicenseAcknowledgement } from '../enums/licenseAcknowledgement' import type { SpdxId } from '../spdx' import type { Attachment } from './attachment' +import { PropertyRepository } from './property' /** * (SPDX) License Expression. @@ -64,11 +65,13 @@ class DisjunctiveLicenseBase { acknowledgement?: LicenseAcknowledgement text?: Attachment #url?: URL | string + properties: PropertyRepository constructor (op: OptionalDisjunctiveLicenseProperties = {}) { this.acknowledgement = op.acknowledgement this.text = op.text this.url = op.url + this.properties = op.properties ?? new PropertyRepository() } get url (): URL | string | undefined { @@ -86,6 +89,7 @@ interface OptionalDisjunctiveLicenseProperties { acknowledgement?: DisjunctiveLicenseBase['acknowledgement'] text?: DisjunctiveLicenseBase['text'] url?: DisjunctiveLicenseBase['url'] + properties?: DisjunctiveLicenseBase['properties'] } export interface OptionalNamedLicenseProperties extends OptionalDisjunctiveLicenseProperties {} diff --git a/src/serialize/json/normalize.ts b/src/serialize/json/normalize.ts index c5004c597..f76f57bd0 100644 --- a/src/serialize/json/normalize.ts +++ b/src/serialize/json/normalize.ts @@ -529,11 +529,12 @@ export class LicenseNormalizer extends BaseJsonNormalizer { } #normalizeNamedLicense (data: Models.NamedLicense, options: NormalizerOptions): Normalized.NamedLicense { + const spec = this._factory.spec const url = escapeUri(data.url?.toString()) return { license: { name: data.name, - acknowledgement: this._factory.spec.supportsLicenseAcknowledgement + acknowledgement: spec.supportsLicenseAcknowledgement ? data.acknowledgement : undefined, text: data.text === undefined @@ -541,17 +542,21 @@ export class LicenseNormalizer extends BaseJsonNormalizer { : this._factory.makeForAttachment().normalize(data.text, options), url: JsonSchema.isIriReference(url) ? url + : undefined, + properties: spec.supportsProperties(data) && data.properties.size > 0 + ? this._factory.makeForProperty().normalizeIterable(data.properties, options) : undefined } } } #normalizeSpdxLicense (data: Models.SpdxLicense, options: NormalizerOptions): Normalized.SpdxLicense { + const spec = this._factory.spec const url = escapeUri(data.url?.toString()) return { license: { id: data.id, - acknowledgement: this._factory.spec.supportsLicenseAcknowledgement + acknowledgement: spec.supportsLicenseAcknowledgement ? data.acknowledgement : undefined, text: data.text === undefined @@ -559,6 +564,9 @@ export class LicenseNormalizer extends BaseJsonNormalizer { : this._factory.makeForAttachment().normalize(data.text, options), url: JsonSchema.isIriReference(url) ? url + : undefined, + properties: spec.supportsProperties(data) && data.properties.size > 0 + ? this._factory.makeForProperty().normalizeIterable(data.properties, options) : undefined } } diff --git a/src/serialize/json/types.ts b/src/serialize/json/types.ts index 384110257..2fbf89490 100644 --- a/src/serialize/json/types.ts +++ b/src/serialize/json/types.ts @@ -196,6 +196,7 @@ export namespace Normalized { acknowledgement?: Enums.LicenseAcknowledgement text?: Attachment url?: string + properties?: Property[] } } @@ -206,6 +207,7 @@ export namespace Normalized { acknowledgement?: Enums.LicenseAcknowledgement text?: Attachment url?: string + properties?: Property[] } } diff --git a/src/serialize/xml/normalize.ts b/src/serialize/xml/normalize.ts index 507db7590..a0ac955ad 100644 --- a/src/serialize/xml/normalize.ts +++ b/src/serialize/xml/normalize.ts @@ -699,12 +699,20 @@ export class LicenseNormalizer extends BaseXmlNormalizer { } #normalizeNamedLicense (data: Models.NamedLicense, options: NormalizerOptions): SimpleXml.Element { - const url = escapeUri(data.url?.toString()) + const spec = this._factory.spec + const url = escapeUri(data.url?.toString()) + 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 return { type: 'element', name: 'license', attributes: { - acknowledgement: this._factory.spec.supportsLicenseAcknowledgement + acknowledgement: spec.supportsLicenseAcknowledgement ? data.acknowledgement : undefined }, @@ -715,18 +723,27 @@ export class LicenseNormalizer extends BaseXmlNormalizer { : this._factory.makeForAttachment().normalize(data.text, options, 'text'), XmlSchema.isAnyURI(url) ? makeTextElement(url, 'url') - : undefined + : undefined, + properties ].filter(isNotUndefined) } } #normalizeSpdxLicense (data: Models.SpdxLicense, options: NormalizerOptions): SimpleXml.Element { - const url = escapeUri(data.url?.toString()) + const spec = this._factory.spec + const url = escapeUri(data.url?.toString()) + 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 return { type: 'element', name: 'license', attributes: { - acknowledgement: this._factory.spec.supportsLicenseAcknowledgement + acknowledgement: spec.supportsLicenseAcknowledgement ? data.acknowledgement : undefined }, @@ -737,7 +754,8 @@ export class LicenseNormalizer extends BaseXmlNormalizer { : this._factory.makeForAttachment().normalize(data.text, options, 'text'), XmlSchema.isAnyURI(url) ? makeTextElement(url, 'url') - : undefined + : undefined, + properties ].filter(isNotUndefined) } } diff --git a/src/spec/_protocol.ts b/src/spec/_protocol.ts index 50020e007..b24cf740d 100644 --- a/src/spec/_protocol.ts +++ b/src/spec/_protocol.ts @@ -18,7 +18,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import type { ComponentType, ExternalReferenceType, HashAlgorithm, Vulnerability } from '../enums' -import type { HashContent } from '../models' +import { type HashContent, NamedLicense, SpdxLicense } from '../models' import type { Format, Version } from './enums' /** @@ -79,6 +79,7 @@ export class _Spec implements _SpecProtocol { readonly #supportsLicenseAcknowledgement: boolean readonly #supportsServices: boolean readonly #supportsToolsComponentsServices: boolean + readonly #supportsLicenseProperties: boolean /* eslint-disable-next-line @typescript-eslint/max-params -- architectural decision */ constructor ( @@ -101,7 +102,8 @@ export class _Spec implements _SpecProtocol { supportsExternalReferenceHashes: boolean, supportsLicenseAcknowledgement: boolean, supportsServices: boolean, - supportsToolsComponentsServices: boolean + supportsToolsComponentsServices: boolean, + supportsLicenseProperties: boolean ) { this.#version = version this.#formats = new Set(formats) @@ -123,6 +125,7 @@ export class _Spec implements _SpecProtocol { this.#supportsLicenseAcknowledgement = supportsLicenseAcknowledgement this.#supportsServices = supportsServices this.#supportsToolsComponentsServices = supportsToolsComponentsServices + this.#supportsLicenseProperties = supportsLicenseProperties } get version (): Version { @@ -166,9 +169,13 @@ export class _Spec implements _SpecProtocol { return this.#requiresComponentVersion } - supportsProperties (): boolean { - // currently a global allow/deny -- might work based on input, in the future - return this.#supportsProperties + supportsProperties (model: any): boolean { + switch (true) { + case model instanceof NamedLicense || model instanceof SpdxLicense: + return this.#supportsLicenseProperties + default: + return this.#supportsProperties + } } get supportsVulnerabilities (): boolean { diff --git a/src/spec/consts.ts b/src/spec/consts.ts index a49ff51ee..271120bd8 100644 --- a/src/spec/consts.ts +++ b/src/spec/consts.ts @@ -89,6 +89,7 @@ export const Spec1dot2: Readonly<_SpecProtocol> = Object.freeze(new _Spec( false, false, true, + false, false )) @@ -154,6 +155,7 @@ export const Spec1dot3: Readonly<_SpecProtocol> = Object.freeze(new _Spec( true, false, true, + false, false )) @@ -226,6 +228,7 @@ export const Spec1dot4: Readonly<_SpecProtocol> = Object.freeze(new _Spec( true, false, true, + false, false )) @@ -327,6 +330,7 @@ export const Spec1dot5: Readonly<_SpecProtocol> = Object.freeze(new _Spec( true, false, true, + true, true )) @@ -433,6 +437,7 @@ export const Spec1dot6: Readonly<_SpecProtocol> = Object.freeze(new _Spec( true, true, true, + true, true )) @@ -547,6 +552,7 @@ export const Spec1dot7: Readonly<_SpecProtocol> = Object.freeze(new _Spec( true, true, true, + true, true )) diff --git a/tests/_data/models.js b/tests/_data/models.js index 321fbd74b..76ba6920d 100644 --- a/tests/_data/models.js +++ b/tests/_data/models.js @@ -158,7 +158,11 @@ function createComplexStructure () { contentType: 'text/plain', encoding: Enums.AttachmentEncoding.Base64 }), - url: 'https://localhost/license' + url: 'https://localhost/license', + properties: new Models.PropertyRepository([ + new Models.Property('a', 'b'), + new Models.Property('primaryLicense', 'true') + ]) })) component.licenses.add((function (license) { license.text = new Models.Attachment('TUlUIExpY2Vuc2UKLi4uClRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLi4u') @@ -167,7 +171,12 @@ function createComplexStructure () { license.url = new URL('https://spdx.org/licenses/MIT.html') license.acknowledgement = Enums.LicenseAcknowledgement.Declared return license - })(new Models.SpdxLicense('MIT'))) + })(new Models.SpdxLicense('MIT', { + properties: new Models.PropertyRepository([ + new Models.Property('c', 'd'), + new Models.Property('primaryLicense', 'false') + ]) + }))) component.publisher = 'the publisher' component.purl = new PackageURL('npm', 'acme', 'dummy-component', '1337-beta', undefined, undefined) component.scope = Enums.ComponentScope.Required diff --git a/tests/_data/normalizeResults/json_sortedLists_spec1.5.json b/tests/_data/normalizeResults/json_sortedLists_spec1.5.json index 359392b83..06995f0f2 100644 --- a/tests/_data/normalizeResults/json_sortedLists_spec1.5.json +++ b/tests/_data/normalizeResults/json_sortedLists_spec1.5.json @@ -309,7 +309,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://localhost/license" + "url": "https://localhost/license", + "properties": [ + { + "name": "a", + "value": "b" + }, + { + "name": "primaryLicense", + "value": "true" + } + ] } }, { @@ -320,7 +330,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://spdx.org/licenses/MIT.html" + "url": "https://spdx.org/licenses/MIT.html", + "properties": [ + { + "name": "c", + "value": "d" + }, + { + "name": "primaryLicense", + "value": "false" + } + ] } } ], diff --git a/tests/_data/normalizeResults/json_sortedLists_spec1.6.json b/tests/_data/normalizeResults/json_sortedLists_spec1.6.json index 57c946944..5d3face98 100644 --- a/tests/_data/normalizeResults/json_sortedLists_spec1.6.json +++ b/tests/_data/normalizeResults/json_sortedLists_spec1.6.json @@ -309,7 +309,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://localhost/license" + "url": "https://localhost/license", + "properties": [ + { + "name": "a", + "value": "b" + }, + { + "name": "primaryLicense", + "value": "true" + } + ] } }, { @@ -321,7 +331,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://spdx.org/licenses/MIT.html" + "url": "https://spdx.org/licenses/MIT.html", + "properties": [ + { + "name": "c", + "value": "d" + }, + { + "name": "primaryLicense", + "value": "false" + } + ] } } ], diff --git a/tests/_data/normalizeResults/json_sortedLists_spec1.7.json b/tests/_data/normalizeResults/json_sortedLists_spec1.7.json index ae23bd552..ba49c0fba 100644 --- a/tests/_data/normalizeResults/json_sortedLists_spec1.7.json +++ b/tests/_data/normalizeResults/json_sortedLists_spec1.7.json @@ -309,7 +309,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://localhost/license" + "url": "https://localhost/license", + "properties": [ + { + "name": "a", + "value": "b" + }, + { + "name": "primaryLicense", + "value": "true" + } + ] } }, { @@ -321,7 +331,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://spdx.org/licenses/MIT.html" + "url": "https://spdx.org/licenses/MIT.html", + "properties": [ + { + "name": "c", + "value": "d" + }, + { + "name": "primaryLicense", + "value": "false" + } + ] } } ], diff --git a/tests/_data/normalizeResults/xml_sortedLists_spec1.5.json b/tests/_data/normalizeResults/xml_sortedLists_spec1.5.json index c93e1334d..81e6020d8 100644 --- a/tests/_data/normalizeResults/xml_sortedLists_spec1.5.json +++ b/tests/_data/normalizeResults/xml_sortedLists_spec1.5.json @@ -967,6 +967,28 @@ "type": "element", "name": "url", "children": "https://localhost/license" + }, + { + "type": "element", + "name": "properties", + "children": [ + { + "type": "element", + "name": "property", + "attributes": { + "name": "a" + }, + "children": "b" + }, + { + "type": "element", + "name": "property", + "attributes": { + "name": "primaryLicense" + }, + "children": "true" + } + ] } ] }, @@ -993,6 +1015,28 @@ "type": "element", "name": "url", "children": "https://spdx.org/licenses/MIT.html" + }, + { + "type": "element", + "name": "properties", + "children": [ + { + "type": "element", + "name": "property", + "attributes": { + "name": "c" + }, + "children": "d" + }, + { + "type": "element", + "name": "property", + "attributes": { + "name": "primaryLicense" + }, + "children": "false" + } + ] } ] } diff --git a/tests/_data/normalizeResults/xml_sortedLists_spec1.6.json b/tests/_data/normalizeResults/xml_sortedLists_spec1.6.json index ed5c34aae..6385a0eeb 100644 --- a/tests/_data/normalizeResults/xml_sortedLists_spec1.6.json +++ b/tests/_data/normalizeResults/xml_sortedLists_spec1.6.json @@ -967,6 +967,28 @@ "type": "element", "name": "url", "children": "https://localhost/license" + }, + { + "type": "element", + "name": "properties", + "children": [ + { + "type": "element", + "name": "property", + "attributes": { + "name": "a" + }, + "children": "b" + }, + { + "type": "element", + "name": "property", + "attributes": { + "name": "primaryLicense" + }, + "children": "true" + } + ] } ] }, @@ -995,6 +1017,28 @@ "type": "element", "name": "url", "children": "https://spdx.org/licenses/MIT.html" + }, + { + "type": "element", + "name": "properties", + "children": [ + { + "type": "element", + "name": "property", + "attributes": { + "name": "c" + }, + "children": "d" + }, + { + "type": "element", + "name": "property", + "attributes": { + "name": "primaryLicense" + }, + "children": "false" + } + ] } ] } diff --git a/tests/_data/normalizeResults/xml_sortedLists_spec1.7.json b/tests/_data/normalizeResults/xml_sortedLists_spec1.7.json index 751d6d801..339a3e936 100644 --- a/tests/_data/normalizeResults/xml_sortedLists_spec1.7.json +++ b/tests/_data/normalizeResults/xml_sortedLists_spec1.7.json @@ -967,6 +967,28 @@ "type": "element", "name": "url", "children": "https://localhost/license" + }, + { + "type": "element", + "name": "properties", + "children": [ + { + "type": "element", + "name": "property", + "attributes": { + "name": "a" + }, + "children": "b" + }, + { + "type": "element", + "name": "property", + "attributes": { + "name": "primaryLicense" + }, + "children": "true" + } + ] } ] }, @@ -995,6 +1017,28 @@ "type": "element", "name": "url", "children": "https://spdx.org/licenses/MIT.html" + }, + { + "type": "element", + "name": "properties", + "children": [ + { + "type": "element", + "name": "property", + "attributes": { + "name": "c" + }, + "children": "d" + }, + { + "type": "element", + "name": "property", + "attributes": { + "name": "primaryLicense" + }, + "children": "false" + } + ] } ] } diff --git a/tests/_data/serializeResults/json_complex_spec1.5.json.bin b/tests/_data/serializeResults/json_complex_spec1.5.json.bin index 2a286217f..fe361a152 100644 --- a/tests/_data/serializeResults/json_complex_spec1.5.json.bin +++ b/tests/_data/serializeResults/json_complex_spec1.5.json.bin @@ -309,7 +309,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://localhost/license" + "url": "https://localhost/license", + "properties": [ + { + "name": "a", + "value": "b" + }, + { + "name": "primaryLicense", + "value": "true" + } + ] } }, { @@ -320,7 +330,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://spdx.org/licenses/MIT.html" + "url": "https://spdx.org/licenses/MIT.html", + "properties": [ + { + "name": "c", + "value": "d" + }, + { + "name": "primaryLicense", + "value": "false" + } + ] } } ], diff --git a/tests/_data/serializeResults/json_complex_spec1.6.json.bin b/tests/_data/serializeResults/json_complex_spec1.6.json.bin index aa520a610..fce78fef1 100644 --- a/tests/_data/serializeResults/json_complex_spec1.6.json.bin +++ b/tests/_data/serializeResults/json_complex_spec1.6.json.bin @@ -309,7 +309,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://localhost/license" + "url": "https://localhost/license", + "properties": [ + { + "name": "a", + "value": "b" + }, + { + "name": "primaryLicense", + "value": "true" + } + ] } }, { @@ -321,7 +331,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://spdx.org/licenses/MIT.html" + "url": "https://spdx.org/licenses/MIT.html", + "properties": [ + { + "name": "c", + "value": "d" + }, + { + "name": "primaryLicense", + "value": "false" + } + ] } } ], diff --git a/tests/_data/serializeResults/json_complex_spec1.7.json.bin b/tests/_data/serializeResults/json_complex_spec1.7.json.bin index ea2542dda..2b5571adf 100644 --- a/tests/_data/serializeResults/json_complex_spec1.7.json.bin +++ b/tests/_data/serializeResults/json_complex_spec1.7.json.bin @@ -309,7 +309,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://localhost/license" + "url": "https://localhost/license", + "properties": [ + { + "name": "a", + "value": "b" + }, + { + "name": "primaryLicense", + "value": "true" + } + ] } }, { @@ -321,7 +331,17 @@ "contentType": "text/plain", "encoding": "base64" }, - "url": "https://spdx.org/licenses/MIT.html" + "url": "https://spdx.org/licenses/MIT.html", + "properties": [ + { + "name": "c", + "value": "d" + }, + { + "name": "primaryLicense", + "value": "false" + } + ] } } ], diff --git a/tests/_data/serializeResults/xml_complex_spec1.5.xml.bin b/tests/_data/serializeResults/xml_complex_spec1.5.xml.bin index 3ad96d85c..27060f7d3 100644 --- a/tests/_data/serializeResults/xml_complex_spec1.5.xml.bin +++ b/tests/_data/serializeResults/xml_complex_spec1.5.xml.bin @@ -215,11 +215,19 @@ some other U29tZQpsaWNlbnNlCnRleHQu https://localhost/license + + b + true + MIT TUlUIExpY2Vuc2UKLi4uClRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLi4u https://spdx.org/licenses/MIT.html + + d + false + ACME corp diff --git a/tests/_data/serializeResults/xml_complex_spec1.6.xml.bin b/tests/_data/serializeResults/xml_complex_spec1.6.xml.bin index ec8a02a19..4c633acb4 100644 --- a/tests/_data/serializeResults/xml_complex_spec1.6.xml.bin +++ b/tests/_data/serializeResults/xml_complex_spec1.6.xml.bin @@ -215,11 +215,19 @@ some other U29tZQpsaWNlbnNlCnRleHQu https://localhost/license + + b + true + MIT TUlUIExpY2Vuc2UKLi4uClRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLi4u https://spdx.org/licenses/MIT.html + + d + false + ACME corp diff --git a/tests/_data/serializeResults/xml_complex_spec1.7.xml.bin b/tests/_data/serializeResults/xml_complex_spec1.7.xml.bin index 8be33b85a..b4865d0cd 100644 --- a/tests/_data/serializeResults/xml_complex_spec1.7.xml.bin +++ b/tests/_data/serializeResults/xml_complex_spec1.7.xml.bin @@ -215,11 +215,19 @@ some other U29tZQpsaWNlbnNlCnRleHQu https://localhost/license + + b + true + MIT TUlUIExpY2Vuc2UKLi4uClRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLi4u https://spdx.org/licenses/MIT.html + + d + false + ACME corp