From 91dcd21244912c78878e226b34847f79bc731a59 Mon Sep 17 00:00:00 2001 From: Rhys Koedijk Date: Wed, 18 Dec 2024 23:04:47 +1300 Subject: [PATCH] Improved accuracy of license expression parsing --- shared/models/spdx/2.3/ILicense.ts | 25 +++++++----- shared/models/spdx/2.3/IPackage.ts | 5 +++ shared/package-lock.json | 43 ++++++++++++++++++++- shared/package.json | 7 +++- shared/spdx/convertSpdxToXlsx.ts | 19 +++++---- ui/components/sbom/SbomDocumentPage.tsx | 2 +- ui/components/sbom/SpdxLicenseTableCard.tsx | 12 +++--- 7 files changed, 85 insertions(+), 28 deletions(-) diff --git a/shared/models/spdx/2.3/ILicense.ts b/shared/models/spdx/2.3/ILicense.ts index d45aeff..1ed07b3 100644 --- a/shared/models/spdx/2.3/ILicense.ts +++ b/shared/models/spdx/2.3/ILicense.ts @@ -1,15 +1,9 @@ -import * as spdxLicenseList from './licenses.json'; +import * as spdxLicenseList from 'spdx-license-list'; export interface ILicense { - reference: string; - isDeprecatedLicenseId: boolean; - detailsUrl: string; - referenceNumber: number; + id: string; name: string; - licenseId: string; - seeAlso: string[]; - isOsiApproved: boolean; - isFsfLibre?: boolean; + url: string; } /** @@ -19,5 +13,16 @@ export interface ILicense { * @returns The licenses */ export function getLicensesFromExpression(licenseExpression: string): ILicense[] | undefined { - return spdxLicenseList.licenses.filter((x: { licenseId: string }) => licenseExpression.includes(x.licenseId)); + return Object.keys(spdxLicenseList) + .filter((id) => + licenseExpression + .split(/\s+/) + .filter((word) => word.length > 0) + .includes(id), + ) + .map((id) => ({ + id, + name: spdxLicenseList[id].name, + url: spdxLicenseList[id].url, + })); } diff --git a/shared/models/spdx/2.3/IPackage.ts b/shared/models/spdx/2.3/IPackage.ts index 546b44f..eee34a8 100644 --- a/shared/models/spdx/2.3/IPackage.ts +++ b/shared/models/spdx/2.3/IPackage.ts @@ -28,6 +28,11 @@ export function getPackageLicenseExpression(pkg: IPackage): string | undefined { return undefined; } +export function getPackageLicenseReferences(pkg: IPackage): string[] { + const expression = getPackageLicenseExpression(pkg) || ''; + return expression.split(/\s+/).filter((word) => word.length > 0); +} + export function getPackageSupplierOrganization(pkg: IPackage): string | undefined { if (pkg.supplier === NOASSERTION) { return undefined; diff --git a/shared/package-lock.json b/shared/package-lock.json index 93e6888..e4f9d76 100644 --- a/shared/package-lock.json +++ b/shared/package-lock.json @@ -9,10 +9,13 @@ "buffer": "^6.0.3", "json-as-xlsx": "^2.5.6", "packageurl-js": "^2.0.1", - "semver": "^7.6.3" + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "spdx-license-list": "^6.9.0" }, "devDependencies": { - "@types/semver": "^7.5.8" + "@types/semver": "^7.5.8", + "@types/spdx-expression-parse": "^3.0.5" } }, "node_modules/@e965/xlsx": { @@ -32,6 +35,12 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/spdx-expression-parse": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/spdx-expression-parse/-/spdx-expression-parse-3.0.5.tgz", + "integrity": "sha512-XrojSCTzVxPAfWeAiw8Hg27OW/4jalE7yiohCHRPprqfPyt2oG+Osy1HstUPMF26cEdno3IeEhv31Pzl0wwsQw==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -206,6 +215,36 @@ "engines": { "node": ">=10" } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==" + }, + "node_modules/spdx-license-list": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/spdx-license-list/-/spdx-license-list-6.9.0.tgz", + "integrity": "sha512-L2jl5vc2j6jxWcNCvcVj/BW9A8yGIG02Dw+IUw0ZxDM70f7Ylf5Hq39appV1BI9yxyWQRpq2TQ1qaXvf+yjkqA==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/shared/package.json b/shared/package.json index 8926b95..1892a21 100644 --- a/shared/package.json +++ b/shared/package.json @@ -5,9 +5,12 @@ "buffer": "^6.0.3", "json-as-xlsx": "^2.5.6", "packageurl-js": "^2.0.1", - "semver": "^7.6.3" + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "spdx-license-list": "^6.9.0" }, "devDependencies": { - "@types/semver": "^7.5.8" + "@types/semver": "^7.5.8", + "@types/spdx-expression-parse": "^3.0.5" } } diff --git a/shared/spdx/convertSpdxToXlsx.ts b/shared/spdx/convertSpdxToXlsx.ts index a82cfe7..3c2fb8c 100644 --- a/shared/spdx/convertSpdxToXlsx.ts +++ b/shared/spdx/convertSpdxToXlsx.ts @@ -15,7 +15,12 @@ import { } from '../models/spdx/2.3/IExternalRef'; import { IFile } from '../models/spdx/2.3/IFile'; import { getLicensesFromExpression, ILicense } from '../models/spdx/2.3/ILicense'; -import { getPackageLicenseExpression, getPackageSupplierOrganization, IPackage } from '../models/spdx/2.3/IPackage'; +import { + getPackageLicenseExpression, + getPackageLicenseReferences, + getPackageSupplierOrganization, + IPackage, +} from '../models/spdx/2.3/IPackage'; import { IRelationship } from '../models/spdx/2.3/IRelationship'; import { parseSpdxSecurityAdvisoriesLegacy } from './parseSpdxSecurityAdvisoriesLegacy'; @@ -57,7 +62,7 @@ export async function convertSpdxToXlsxAsync(spdx: IDocument): Promise { .filter((licenseExpression): licenseExpression is string => !!licenseExpression) .flatMap((licenseExpression: string) => getLicensesFromExpression(licenseExpression)) .filter((license): license is ILicense => !!license) - .distinctBy((license: ILicense) => license.licenseId); + .distinctBy((license: ILicense) => license.id); const suppliers = packages .map((pkg) => getPackageSupplierOrganization(pkg)) .filter((supplier): supplier is string => !!supplier) @@ -254,20 +259,20 @@ export async function convertSpdxToXlsxAsync(spdx: IDocument): Promise { { label: 'Risk Reason', value: 'riskReasons' }, ], content: licenses - .orderBy((license: ILicense) => license.licenseId) + .orderBy((license: ILicense) => license.id) .map((license: ILicense) => { const packagesWithLicense = packages - .filter((p) => getPackageLicenseExpression(p)?.includes(license.licenseId)) + .filter((p) => getPackageLicenseReferences(p).includes(license.id)) .map((p) => `${p.name || ''}@${p.versionInfo || ''}`) .distinct(); - const licenseRisk = getLicenseRiskAssessment(license.licenseId); + const licenseRisk = getLicenseRiskAssessment(license.id); return { - id: license.licenseId, + id: license.id, name: license.name, packages: packagesWithLicense.length, riskSeverity: (licenseRisk?.severity || LicenseRiskSeverity.Low).toPascalCase(), riskReasons: licenseRisk?.reasons?.join('; ') || '', - url: license.reference, + url: license.url || '', }; }), }; diff --git a/ui/components/sbom/SbomDocumentPage.tsx b/ui/components/sbom/SbomDocumentPage.tsx index 06b4b26..ecf7fe6 100644 --- a/ui/components/sbom/SbomDocumentPage.tsx +++ b/ui/components/sbom/SbomDocumentPage.tsx @@ -82,7 +82,7 @@ export class SbomDocumentPage extends React.Component { .filter((licenseExpression): licenseExpression is string => !!licenseExpression) .flatMap((licenseExpression: string) => getLicensesFromExpression(licenseExpression)) .filter((license): license is ILicense => !!license) - .distinctBy((license: ILicense) => license.licenseId); + .distinctBy((license: ILicense) => license.id); const suppliers = packages .map((pkg) => getPackageSupplierOrganization(pkg)) .filter((supplier): supplier is string => !!supplier) diff --git a/ui/components/sbom/SpdxLicenseTableCard.tsx b/ui/components/sbom/SpdxLicenseTableCard.tsx index ee7cc4c..901b8b4 100644 --- a/ui/components/sbom/SpdxLicenseTableCard.tsx +++ b/ui/components/sbom/SpdxLicenseTableCard.tsx @@ -25,7 +25,7 @@ import { getSeverityByName } from '../../../shared/models/severity/Severities'; import { IDocument } from '../../../shared/models/spdx/2.3/IDocument'; import { getExternalRefPackageManagerUrl } from '../../../shared/models/spdx/2.3/IExternalRef'; import { ILicense } from '../../../shared/models/spdx/2.3/ILicense'; -import { getPackageLicenseExpression } from '../../../shared/models/spdx/2.3/IPackage'; +import { getPackageLicenseReferences } from '../../../shared/models/spdx/2.3/IPackage'; const MAX_PACKAGES_VISIBLE = 3; @@ -77,10 +77,10 @@ export class SpdxLicenseTableCard extends React.Component { ); const rawTableItems: ILicenseTableItem[] = props.licenses - ?.orderBy((license: ILicense) => license.licenseId) + ?.orderBy((license: ILicense) => license.id) ?.map((license: ILicense) => { const packagesWithLicense = props.document.packages - ?.filter((p) => getPackageLicenseExpression(p)?.includes(license.licenseId)) + ?.filter((p) => getPackageLicenseReferences(p)?.includes(license.id)) ?.map((p) => { return { name: p.name || '', @@ -88,16 +88,16 @@ export class SpdxLicenseTableCard extends React.Component { url: getExternalRefPackageManagerUrl(p.externalRefs), }; }); - const licenseRisk = getLicenseRiskAssessment(license.licenseId); + const licenseRisk = getLicenseRiskAssessment(license.id); return { - id: license.licenseId, + id: license.id, name: license.name, packagesTotal: packagesWithLicense.length, packagesVisible: new ObservableValue(MAX_PACKAGES_VISIBLE), packages: packagesWithLicense, riskSeverity: getSeverityByName(licenseRisk?.severity || LicenseRiskSeverity.Low), riskReasons: licenseRisk?.reasons || [], - url: license.reference, + url: license.url || '', }; }) || [];