Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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)
--------------------

Expand Down
34 changes: 25 additions & 9 deletions docs/policies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------
Expand Down
36 changes: 18 additions & 18 deletions scanpipe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2502,6 +2502,7 @@ class Meta:
"""

license_expression_field = None
license_expression_spdx_field = None

class Compliance(models.TextChoices):
OK = "ok"
Expand Down Expand Up @@ -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):
Expand Down
43 changes: 34 additions & 9 deletions scanpipe/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}
19 changes: 16 additions & 3 deletions scanpipe/tests/data/policies/policies.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions scanpipe/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down