You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/07/24 08:05:48 UTC
(tooling-trusted-releases) branch arm updated: Fix named license showing as category X
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/arm by this push:
new ab3e2add Fix named license showing as category X
ab3e2add is described below
commit ab3e2add7a352126b80b0349e9aad6db49146f43
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Fri Jul 24 09:05:21 2026 +0100
Fix named license showing as category X
---
atr/sbom/constants/licenses.py | 94 ++++++++++++++++++++++++++++++++++++++++
atr/sbom/licenses.py | 39 ++++++++++++++++-
tests/unit/test_sbom_licenses.py | 49 +++++++++++++++++++++
3 files changed, 180 insertions(+), 2 deletions(-)
diff --git a/atr/sbom/constants/licenses.py b/atr/sbom/constants/licenses.py
index 0e0fcee6..7ce34b19 100644
--- a/atr/sbom/constants/licenses.py
+++ b/atr/sbom/constants/licenses.py
@@ -150,6 +150,100 @@ LICENSES: Final[dict[str, list[str]]] = {
],
}
+# The SPDX name for each categorised licence, so that a component declaring its licence by
+# name instead of by identifier can still be categorised
+LICENSE_NAMES: Final[dict[str, str]] = {
+ "0BSD": "BSD Zero Clause License",
+ "AFL-3.0": "Academic Free License v3.0",
+ "AGPL-3.0-only": "GNU Affero General Public License v3.0 only",
+ "AGPL-3.0-or-later": "GNU Affero General Public License v3.0 or later",
+ "Apache-1.1": "Apache License 1.1",
+ "Apache-2.0": "Apache License 2.0",
+ "APAFML": "Adobe Postscript AFM License",
+ "Bitstream-Vera": "Bitstream Vera Font License",
+ "BlueOak-1.0.0": "Blue Oak Model License 1.0.0",
+ "BSD-2-Clause": 'BSD 2-Clause "Simplified" License',
+ "BSD-3-Clause": 'BSD 3-Clause "New" or "Revised" License',
+ "BSD-3-Clause-LBNL": "Lawrence Berkeley National Labs BSD variant license",
+ "BSD-4-Clause": 'BSD 4-Clause "Original" or "Old" License',
+ "BSD-4-Clause-UC": "BSD-4-Clause (University of California-Specific)",
+ "BSL-1.0": "Boost Software License 1.0",
+ "BUSL-1.1": "Business Source License 1.1",
+ "CC-BY-2.5": "Creative Commons Attribution 2.5 Generic",
+ "CC-BY-3.0": "Creative Commons Attribution 3.0 Unported",
+ "CC-BY-4.0": "Creative Commons Attribution 4.0 International",
+ "CC-BY-NC-4.0": "Creative Commons Attribution Non Commercial 4.0 International",
+ "CC-BY-SA-2.5": "Creative Commons Attribution Share Alike 2.5 Generic",
+ "CC-BY-SA-3.0": "Creative Commons Attribution Share Alike 3.0 Unported",
+ "CC-BY-SA-4.0": "Creative Commons Attribution Share Alike 4.0 International",
+ "CC-PDDC": "Creative Commons Public Domain Dedication and Certification",
+ "CC-PDM-1.0": "Creative Commons Public Domain Mark 1.0 Universal",
+ "CC0-1.0": "Creative Commons Zero v1.0 Universal",
+ "CDDL-1.0": "Common Development and Distribution License 1.0",
+ "CDDL-1.1": "Common Development and Distribution License 1.1",
+ "CPL-1.0": "Common Public License 1.0",
+ "CPOL-1.02": "Code Project Open License 1.02",
+ "DOC": "DOC License",
+ "EPICS": "EPICS Open License",
+ "EPL-1.0": "Eclipse Public License 1.0",
+ "EPL-2.0": "Eclipse Public License 2.0",
+ "ErlPL-1.1": "Erlang Public License v1.1",
+ "FSFAP": "FSF All Permissive License",
+ "GPL-1.0-only": "GNU General Public License v1.0 only",
+ "GPL-1.0-or-later": "GNU General Public License v1.0 or later",
+ "GPL-2.0-only": "GNU General Public License v2.0 only",
+ "GPL-2.0-or-later": "GNU General Public License v2.0 or later",
+ "GPL-3.0-only": "GNU General Public License v3.0 only",
+ "GPL-3.0-or-later": "GNU General Public License v3.0 or later",
+ "HPND": "Historical Permission Notice and Disclaimer",
+ "ICU": "ICU License",
+ "IPA": "IPA Font License",
+ "IPL-1.0": "IBM Public License v1.0",
+ "ISC": "ISC License",
+ "JSON": "JSON License",
+ "LGPL-2.0-only": "GNU Library General Public License v2 only",
+ "LGPL-2.0-or-later": "GNU Library General Public License v2 or later",
+ "LGPL-2.1-only": "GNU Lesser General Public License v2.1 only",
+ "LGPL-2.1-or-later": "GNU Lesser General Public License v2.1 or later",
+ "LGPL-3.0-only": "GNU Lesser General Public License v3.0 only",
+ "LGPL-3.0-or-later": "GNU Lesser General Public License v3.0 or later",
+ "Libpng": "libpng License",
+ "MIT": "MIT License",
+ "MIT-0": "MIT No Attribution",
+ "MPL-1.0": "Mozilla Public License 1.0",
+ "MPL-1.1": "Mozilla Public License 1.1",
+ "MPL-2.0": "Mozilla Public License 2.0",
+ "MS-LPL": "Microsoft Limited Public License",
+ "MS-PL": "Microsoft Public License",
+ "MulanPSL-2.0": "Mulan Permissive Software License, Version 2",
+ "NCSA": "University of Illinois/NCSA Open Source License",
+ "NPL-1.0": "Netscape Public License v1.0",
+ "NPL-1.1": "Netscape Public License v1.1",
+ "OFL-1.1": "SIL Open Font License 1.1",
+ "OGL-UK-3.0": "Open Government Licence v3.0",
+ "OSL-3.0": "Open Software License 3.0",
+ "PHP-3.01": "PHP License v3.01",
+ "PostgreSQL": "PostgreSQL License",
+ "PSF-2.0": "Python Software Foundation License 2.0",
+ "Python-2.0": "Python License 2.0",
+ "QPL-1.0": "Q Public License 1.0",
+ "Ruby": "Ruby License",
+ "Sleepycat": "Sleepycat License",
+ "SMLNJ": "Standard ML of New Jersey License",
+ "SPL-1.0": "Sun Public License v1.0",
+ "SSPL-1.0": "Server Side Public License, v 1",
+ "TCL": "TCL/TK License",
+ "Ubuntu-font-1.0": "Ubuntu Font Licence v1.0",
+ "Unicode-DFS-2016": "Unicode License Agreement - Data Files and Software (2016)",
+ "Unlicense": "The Unlicense",
+ "UPL-1.0": "Universal Permissive License v1.0",
+ "W3C": "W3C Software Notice and License (2002-12-31)",
+ "WTFPL": "Do What The F*ck You Want To Public License",
+ "Xnet": "X.Net License",
+ "Zlib": "zlib License",
+ "ZPL-2.0": "Zope Public License 2.0",
+}
+
CATEGORY_A_LICENSES_FOLD: Final[frozenset[str]] = frozenset(
value.casefold() for value in LICENSES["CATEGORY_A_LICENSES"]
)
diff --git a/atr/sbom/licenses.py b/atr/sbom/licenses.py
index 2322a13a..b7d999e1 100644
--- a/atr/sbom/licenses.py
+++ b/atr/sbom/licenses.py
@@ -17,7 +17,8 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+import re
+from typing import TYPE_CHECKING, Final
from cyclonedx.model.license import DisjunctiveLicense, License, LicenseExpression
@@ -28,6 +29,12 @@ if TYPE_CHECKING:
from . import constants, models
from .spdx import license_expression_atoms
+# Words that carry no distinguishing meaning, so that "Apache License, Version 2.0" and the SPDX
+# name "Apache License 2.0" reduce to the same thing
+_NAME_NOISE: Final[frozenset[str]] = frozenset({"the", "license", "licence", "licenses", "licences", "version", "v"})
+_NAME_SPLIT: Final[re.Pattern[str]] = re.compile(r"[^0-9a-z]+")
+_NAME_VERSION: Final[re.Pattern[str]] = re.compile(r"^v(\d.*)$")
+
def check(
bom_value: Bom,
@@ -68,7 +75,7 @@ def check(
got_error = False
any_unknown = parse_failed
for atom in atoms:
- folded = atom.casefold()
+ folded = _folded_atom(atom)
if folded in constants.licenses.CATEGORY_A_LICENSES_FOLD:
continue
if folded in constants.licenses.CATEGORY_B_LICENSES_FOLD:
@@ -127,3 +134,31 @@ def expression(license_choice: License) -> str | None:
if isinstance(license_choice, DisjunctiveLicense):
return license_choice.id or license_choice.name
return None
+
+
+def _folded_atom(atom: str) -> str:
+ resolved = _NAME_TO_ID.get(_normalised_name(atom))
+ return (resolved if (resolved is not None) else atom).casefold()
+
+
+def _name_to_id() -> dict[str, str]:
+ index: dict[str, str] = {}
+ for license_id, name in constants.licenses.LICENSE_NAMES.items():
+ index[_normalised_name(name)] = license_id
+ for license_ids in constants.licenses.LICENSES.values():
+ for license_id in license_ids:
+ index.setdefault(_normalised_name(license_id), license_id)
+ return index
+
+
+def _normalised_name(text: str) -> str:
+ words = []
+ for word in _NAME_SPLIT.split(text.casefold()):
+ if (not word) or (word in _NAME_NOISE):
+ continue
+ version = _NAME_VERSION.match(word)
+ words.append(version.group(1) if version else word)
+ return " ".join(words)
+
+
+_NAME_TO_ID: Final[dict[str, str]] = _name_to_id()
diff --git a/tests/unit/test_sbom_licenses.py b/tests/unit/test_sbom_licenses.py
index 490d8d2e..45f851a5 100644
--- a/tests/unit/test_sbom_licenses.py
+++ b/tests/unit/test_sbom_licenses.py
@@ -17,6 +17,7 @@
import cyclonedx.model.license as cdx_license
+import atr.sbom.constants as constants
import atr.sbom.licenses as licenses
import atr.sbom.models as models
import tests.unit.sboms as sboms
@@ -49,3 +50,51 @@ def test_check() -> None:
("forbidden", models.licenses.Category.X, False),
("named", models.licenses.Category.X, True),
]
+
+
+def test_check_categorises_a_licence_named_in_full() -> None:
+ # Components that give no SPDX identifier name their licence however their build file spelled
+ # it, so the category must not depend on that spelling
+ good, warnings, errors = licenses.check(
+ sboms.with_components(
+ {
+ "type": "library",
+ "name": "maven-style",
+ "licenses": [{"license": {"name": "Apache License, Version 2.0"}}],
+ },
+ {"type": "library", "name": "spdx-style", "licenses": [{"license": {"name": "Apache License 2.0"}}]},
+ {"type": "library", "name": "definite", "licenses": [{"license": {"name": "The MIT License"}}]},
+ {
+ "type": "library",
+ "name": "abbreviated",
+ "licenses": [{"license": {"name": "Eclipse Public License v2.0"}}],
+ },
+ ),
+ include_all=True,
+ )
+
+ assert sorted(issue.component_name for issue in good) == ["definite", "maven-style", "spdx-style"]
+ assert [issue.component_name for issue in warnings] == ["abbreviated"]
+ assert [issue.category for issue in warnings] == [models.licenses.Category.B]
+ assert errors == []
+
+
+def test_check_reports_the_licence_as_the_component_declared_it() -> None:
+ good, _warnings, _errors = licenses.check(
+ sboms.with_components(
+ {
+ "type": "library",
+ "name": "maven-style",
+ "licenses": [{"license": {"name": "Apache License, Version 2.0"}}],
+ },
+ ),
+ include_all=True,
+ )
+
+ assert [issue.license_expression for issue in good] == ["Apache License, Version 2.0"]
+
+
+def test_license_names_resolve_to_exactly_one_identifier() -> None:
+ names = [licenses._normalised_name(name) for name in constants.licenses.LICENSE_NAMES.values()]
+
+ assert len(names) == len(set(names))
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org