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