Skip to content

Commit aa2c786

Browse files
authored
Add support for SPDX license identifiers in license policies file #1348 (#1714)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 1292e1a commit aa2c786

File tree

6 files changed

+108
-39
lines changed

6 files changed

+108
-39
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
v35.2.0 (unreleased)
5+
--------------------
6+
7+
- Add support for SPDX license identifiers as ``license_key`` in license policies
8+
``policies.yml`` file.
9+
https://github.com/aboutcode-org/scancode.io/issues/1348
10+
411
v35.1.0 (2025-07-02)
512
--------------------
613

docs/policies.rst

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,39 @@ structure similar to the following:
2020
- license_key: mit
2121
label: Approved License
2222
compliance_alert: ''
23+
2324
- license_key: mpl-2.0
2425
label: Restricted License
2526
compliance_alert: warning
27+
2628
- license_key: gpl-3.0
2729
label: Prohibited License
2830
compliance_alert: error
2931
30-
- In the example above, licenses are referenced by the ``license_key``,
31-
such as `mit` and `gpl-3.0`, which represent the ScanCode license keys used to
32-
match against licenses detected in scan results.
33-
- Each policy is defined with a ``label`` and a ``compliance_alert``.
34-
You can customize the labels as desired.
35-
- The ``compliance_alert`` field accepts three values:
32+
- license_key: OFL-1.1
33+
compliance_alert: warning
34+
35+
- license_key: LicenseRef-scancode-public-domain
36+
compliance_alert: ''
37+
38+
- license_key: LicenseRef-scancode-unknown-license-reference
39+
compliance_alert: error
40+
41+
- In the example above, licenses are referenced using the ``license_key`` field.
42+
These keys can be either **ScanCode license identifiers** (e.g., "mit", "gpl-3.0"),
43+
or **SPDX license identifiers** (e.g., "OFL-1.1",
44+
"LicenseRef-scancode-public-domain").
45+
These values are used to match against the licenses detected in scan results.
46+
47+
- Each policy entry includes a ``label`` and a ``compliance_alert`` field.
48+
The ``label`` is a customizable description used for display or reporting purposes.
49+
50+
- The ``compliance_alert`` field determines the severity level for a license and
51+
supports the following values:
3652

37-
- ``''`` (empty string)
38-
- ``warning``
39-
- ``error``
53+
- ``''`` (empty string) — No action needed; the license is approved.
54+
- ``warning`` — Use with caution; the license may have some restrictions.
55+
- ``error`` — The license is prohibited or incompatible with your policy.
4056

4157
App Policies
4258
------------

scanpipe/models.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2502,6 +2502,7 @@ class Meta:
25022502
"""
25032503

25042504
license_expression_field = None
2505+
license_expression_spdx_field = None
25052506

25062507
class Compliance(models.TextChoices):
25072508
OK = "ok"
@@ -2579,30 +2580,29 @@ def compute_compliance_alert(self):
25792580
Chooses the most severe compliance_alert found among licenses.
25802581
"""
25812582
license_expression = getattr(self, self.license_expression_field, "")
2582-
if not license_expression:
2583-
return ""
2584-
25852583
policy_index = self.policy_index
2586-
if not policy_index:
2587-
return
2584+
if not license_expression or not policy_index:
2585+
return ""
25882586

25892587
licensing = get_licensing()
2590-
parsed = licensing.parse(license_expression, simple=True)
2591-
license_keys = licensing.license_keys(parsed)
2588+
parsed_symbols = licensing.parse(license_expression, simple=True).symbols
25922589

2593-
alerts = []
2594-
for license_key in license_keys:
2595-
if policy := policy_index.get(license_key):
2596-
alerts.append(policy.get("compliance_alert") or self.Compliance.OK)
2597-
else:
2598-
alerts.append(self.Compliance.MISSING)
2590+
alerts = [
2591+
self.get_alert_for_symbol(policy_index, symbol) for symbol in parsed_symbols
2592+
]
2593+
most_severe_alert = max(alerts, key=self.COMPLIANCE_SEVERITY_MAP.get)
2594+
return most_severe_alert or self.Compliance.OK
2595+
2596+
def get_alert_for_symbol(self, policy_index, symbol):
2597+
"""Retrieve the compliance alert for a given license symbol."""
2598+
license_key = symbol.key
2599+
spdx_key = getattr(symbol.wrapped, "spdx_license_key", None)
25992600

2600-
if not alerts:
2601-
return self.Compliance.OK
2601+
policy = policy_index.get(license_key) or policy_index.get(spdx_key)
2602+
if policy:
2603+
return policy.get("compliance_alert") or self.Compliance.OK
26022604

2603-
# Return the most severe alert based on the defined severity
2604-
severity = self.COMPLIANCE_SEVERITY_MAP.get
2605-
return max(alerts, key=severity)
2605+
return self.Compliance.MISSING
26062606

26072607

26082608
class FileClassifierFieldsModelMixin(models.Model):

scanpipe/tests/__init__.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -333,26 +333,51 @@ def make_mock_response(url, content=b"\x00", status_code=200, headers=None):
333333
"label": "Prohibited License",
334334
"compliance_alert": "error",
335335
},
336+
{
337+
"license_key": "OFL-1.1",
338+
"compliance_alert": "warning",
339+
},
340+
{
341+
"license_key": "LicenseRef-scancode-public-domain",
342+
"compliance_alert": "ok",
343+
},
344+
{
345+
"license_key": "LicenseRef-scancode-unknown-license-reference",
346+
"compliance_alert": "error",
347+
},
336348
]
337349

350+
338351
global_policies = {
339352
"license_policies": license_policies,
340353
}
341354

342355
license_policies_index = {
343-
"gpl-3.0": {
344-
"compliance_alert": "error",
345-
"label": "Prohibited License",
346-
"license_key": "gpl-3.0",
347-
},
348356
"apache-2.0": {
349-
"compliance_alert": "",
350-
"label": "Approved License",
351357
"license_key": "apache-2.0",
358+
"label": "Approved License",
359+
"compliance_alert": "",
352360
},
353361
"mpl-2.0": {
354-
"compliance_alert": "warning",
355-
"label": "Restricted License",
356362
"license_key": "mpl-2.0",
363+
"label": "Restricted License",
364+
"compliance_alert": "warning",
365+
},
366+
"gpl-3.0": {
367+
"license_key": "gpl-3.0",
368+
"label": "Prohibited License",
369+
"compliance_alert": "error",
370+
},
371+
"OFL-1.1": {
372+
"license_key": "OFL-1.1",
373+
"compliance_alert": "warning",
374+
},
375+
"LicenseRef-scancode-public-domain": {
376+
"license_key": "LicenseRef-scancode-public-domain",
377+
"compliance_alert": "ok",
378+
},
379+
"LicenseRef-scancode-unknown-license-reference": {
380+
"license_key": "LicenseRef-scancode-unknown-license-reference",
381+
"compliance_alert": "error",
357382
},
358383
}
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
license_policies:
2-
- license_key: apache-2.0
2+
# AboutCode license keys
3+
- license_key: apache-2.0
34
label: Approved License
45
compliance_alert: ''
5-
- license_key: mpl-2.0
6+
7+
- license_key: mpl-2.0
68
label: Restricted License
79
compliance_alert: warning
8-
- license_key: gpl-3.0
10+
11+
- license_key: gpl-3.0
912
label: Prohibited License
1013
compliance_alert: error
14+
15+
# SPDX license keys
16+
- license_key: OFL-1.1
17+
compliance_alert: warning
18+
19+
- license_key: LicenseRef-scancode-public-domain
20+
compliance_alert: ok
21+
22+
- license_key: LicenseRef-scancode-unknown-license-reference
23+
compliance_alert: error

scanpipe/tests/test_models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,6 +1576,14 @@ def test_scanpipe_codebase_resource_model_compliance_alert(self):
15761576
resource.update(detected_license_expression=license_expression)
15771577
self.assertEqual("error", resource.compliance_alert)
15781578

1579+
license_expression = "LicenseRef-scancode-unknown-license-reference"
1580+
resource.update(detected_license_expression=license_expression)
1581+
self.assertEqual("error", resource.compliance_alert)
1582+
1583+
license_expression = "OFL-1.1 AND apache-2.0"
1584+
resource.update(detected_license_expression=license_expression)
1585+
self.assertEqual("warning", resource.compliance_alert)
1586+
15791587
# Reset the index value
15801588
scanpipe_app.license_policies_index = None
15811589

0 commit comments

Comments
 (0)