From 42578561588b8c16ed7f7fa9f69d32e71a31c5bd Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 3 Jul 2025 16:51:17 +0400 Subject: [PATCH] Add support for SPDX license identifiers in license policies file #1348 Signed-off-by: tdruez --- CHANGELOG.rst | 7 ++++ docs/policies.rst | 34 +++++++++++++----- scanpipe/models.py | 36 +++++++++---------- scanpipe/tests/__init__.py | 43 ++++++++++++++++++----- scanpipe/tests/data/policies/policies.yml | 19 ++++++++-- scanpipe/tests/test_models.py | 8 +++++ 6 files changed, 108 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cf2a4e738b..36a4817549 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +v35.2.0 (unreleased) +-------------------- + +- Add support for SPDX license identifiers as ``license_key`` in license policies + ``policies.yml`` file. + https://github.com/aboutcode-org/scancode.io/issues/1348 + v35.1.0 (2025-07-02) -------------------- diff --git a/docs/policies.rst b/docs/policies.rst index 52060cc944..0f2268832e 100644 --- a/docs/policies.rst +++ b/docs/policies.rst @@ -20,23 +20,39 @@ structure similar to the following: - license_key: mit label: Approved License compliance_alert: '' + - license_key: mpl-2.0 label: Restricted License compliance_alert: warning + - license_key: gpl-3.0 label: Prohibited License compliance_alert: error -- In the example above, licenses are referenced by the ``license_key``, - such as `mit` and `gpl-3.0`, which represent the ScanCode license keys used to - match against licenses detected in scan results. -- Each policy is defined with a ``label`` and a ``compliance_alert``. - You can customize the labels as desired. -- The ``compliance_alert`` field accepts three values: + - license_key: OFL-1.1 + compliance_alert: warning + + - license_key: LicenseRef-scancode-public-domain + compliance_alert: '' + + - license_key: LicenseRef-scancode-unknown-license-reference + compliance_alert: error + +- In the example above, licenses are referenced using the ``license_key`` field. + These keys can be either **ScanCode license identifiers** (e.g., "mit", "gpl-3.0"), + or **SPDX license identifiers** (e.g., "OFL-1.1", + "LicenseRef-scancode-public-domain"). + These values are used to match against the licenses detected in scan results. + +- Each policy entry includes a ``label`` and a ``compliance_alert`` field. + The ``label`` is a customizable description used for display or reporting purposes. + +- The ``compliance_alert`` field determines the severity level for a license and + supports the following values: - - ``''`` (empty string) - - ``warning`` - - ``error`` + - ``''`` (empty string) — No action needed; the license is approved. + - ``warning`` — Use with caution; the license may have some restrictions. + - ``error`` — The license is prohibited or incompatible with your policy. App Policies ------------ diff --git a/scanpipe/models.py b/scanpipe/models.py index 7dddef33aa..fe50d57b22 100644 --- a/scanpipe/models.py +++ b/scanpipe/models.py @@ -2502,6 +2502,7 @@ class Meta: """ license_expression_field = None + license_expression_spdx_field = None class Compliance(models.TextChoices): OK = "ok" @@ -2579,30 +2580,29 @@ def compute_compliance_alert(self): Chooses the most severe compliance_alert found among licenses. """ license_expression = getattr(self, self.license_expression_field, "") - if not license_expression: - return "" - policy_index = self.policy_index - if not policy_index: - return + if not license_expression or not policy_index: + return "" licensing = get_licensing() - parsed = licensing.parse(license_expression, simple=True) - license_keys = licensing.license_keys(parsed) + parsed_symbols = licensing.parse(license_expression, simple=True).symbols - alerts = [] - for license_key in license_keys: - if policy := policy_index.get(license_key): - alerts.append(policy.get("compliance_alert") or self.Compliance.OK) - else: - alerts.append(self.Compliance.MISSING) + alerts = [ + self.get_alert_for_symbol(policy_index, symbol) for symbol in parsed_symbols + ] + most_severe_alert = max(alerts, key=self.COMPLIANCE_SEVERITY_MAP.get) + return most_severe_alert or self.Compliance.OK + + def get_alert_for_symbol(self, policy_index, symbol): + """Retrieve the compliance alert for a given license symbol.""" + license_key = symbol.key + spdx_key = getattr(symbol.wrapped, "spdx_license_key", None) - if not alerts: - return self.Compliance.OK + policy = policy_index.get(license_key) or policy_index.get(spdx_key) + if policy: + return policy.get("compliance_alert") or self.Compliance.OK - # Return the most severe alert based on the defined severity - severity = self.COMPLIANCE_SEVERITY_MAP.get - return max(alerts, key=severity) + return self.Compliance.MISSING class FileClassifierFieldsModelMixin(models.Model): diff --git a/scanpipe/tests/__init__.py b/scanpipe/tests/__init__.py index 9f57c4abfc..19d5b43876 100644 --- a/scanpipe/tests/__init__.py +++ b/scanpipe/tests/__init__.py @@ -333,26 +333,51 @@ def make_mock_response(url, content=b"\x00", status_code=200, headers=None): "label": "Prohibited License", "compliance_alert": "error", }, + { + "license_key": "OFL-1.1", + "compliance_alert": "warning", + }, + { + "license_key": "LicenseRef-scancode-public-domain", + "compliance_alert": "ok", + }, + { + "license_key": "LicenseRef-scancode-unknown-license-reference", + "compliance_alert": "error", + }, ] + global_policies = { "license_policies": license_policies, } license_policies_index = { - "gpl-3.0": { - "compliance_alert": "error", - "label": "Prohibited License", - "license_key": "gpl-3.0", - }, "apache-2.0": { - "compliance_alert": "", - "label": "Approved License", "license_key": "apache-2.0", + "label": "Approved License", + "compliance_alert": "", }, "mpl-2.0": { - "compliance_alert": "warning", - "label": "Restricted License", "license_key": "mpl-2.0", + "label": "Restricted License", + "compliance_alert": "warning", + }, + "gpl-3.0": { + "license_key": "gpl-3.0", + "label": "Prohibited License", + "compliance_alert": "error", + }, + "OFL-1.1": { + "license_key": "OFL-1.1", + "compliance_alert": "warning", + }, + "LicenseRef-scancode-public-domain": { + "license_key": "LicenseRef-scancode-public-domain", + "compliance_alert": "ok", + }, + "LicenseRef-scancode-unknown-license-reference": { + "license_key": "LicenseRef-scancode-unknown-license-reference", + "compliance_alert": "error", }, } diff --git a/scanpipe/tests/data/policies/policies.yml b/scanpipe/tests/data/policies/policies.yml index 34876c2bcc..65f24e65d2 100644 --- a/scanpipe/tests/data/policies/policies.yml +++ b/scanpipe/tests/data/policies/policies.yml @@ -1,10 +1,23 @@ license_policies: -- license_key: apache-2.0 + # AboutCode license keys + - license_key: apache-2.0 label: Approved License compliance_alert: '' -- license_key: mpl-2.0 + + - license_key: mpl-2.0 label: Restricted License compliance_alert: warning -- license_key: gpl-3.0 + + - license_key: gpl-3.0 label: Prohibited License compliance_alert: error + + # SPDX license keys + - license_key: OFL-1.1 + compliance_alert: warning + + - license_key: LicenseRef-scancode-public-domain + compliance_alert: ok + + - license_key: LicenseRef-scancode-unknown-license-reference + compliance_alert: error diff --git a/scanpipe/tests/test_models.py b/scanpipe/tests/test_models.py index f1252942a1..d0968f5235 100644 --- a/scanpipe/tests/test_models.py +++ b/scanpipe/tests/test_models.py @@ -1576,6 +1576,14 @@ def test_scanpipe_codebase_resource_model_compliance_alert(self): resource.update(detected_license_expression=license_expression) self.assertEqual("error", resource.compliance_alert) + license_expression = "LicenseRef-scancode-unknown-license-reference" + resource.update(detected_license_expression=license_expression) + self.assertEqual("error", resource.compliance_alert) + + license_expression = "OFL-1.1 AND apache-2.0" + resource.update(detected_license_expression=license_expression) + self.assertEqual("warning", resource.compliance_alert) + # Reset the index value scanpipe_app.license_policies_index = None