Skip to content

Commit 7a98ebc

Browse files
authored
Add compliance support based on OpenSSF Scorecard score (#1800)
Signed-off-by: NucleonGodX <racerpro41@gmail.com>
1 parent 054605c commit 7a98ebc

File tree

14 files changed

+397
-2
lines changed

14 files changed

+397
-2
lines changed

CHANGELOG.rst

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

44
v35.2.0 (2025-08-01)
55
--------------------
6+
- Enhanced scorecard compliance support with:
7+
* New ``scorecard_compliance_alert`` in project ``extra_data``.
8+
* ``/api/projects/{id}/scorecard_compliance/`` API endpoint.
9+
* Scorecard compliance integration in ``check-compliance`` management command.
10+
* UI template support for scorecard compliance alert.
11+
* ``evaluate_scorecard_compliance()`` pipe function for compliance evaluation.
12+
https://github.com/aboutcode-org/scancode.io/pull/1800
613

714
- Refactor policies implementation to support more than licenses.
815
The entire ``policies`` data is now stored on the ``ScanPipeConfig`` in place of the

docs/policies.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,43 @@ Accepted values for the alert level:
9191
- ``warning``
9292
- ``error``
9393

94+
Creating Scorecard Thresholds Files
95+
-----------------------------------
96+
97+
A valid scorecard thresholds file is required to **enable OpenSSF Scorecard compliance features**.
98+
99+
The scorecard thresholds file, by default named ``policies.yml``, is a **YAML file** with a
100+
structure similar to the following:
101+
102+
.. code-block:: yaml
103+
104+
scorecard_score_thresholds:
105+
9.0: ok
106+
7.0: warning
107+
0: error
108+
109+
- In the example above, the keys ``9.0``, ``7.0``, and ``0`` are numeric threshold values
110+
representing **minimum scorecard scores**.
111+
- The values ``error``, ``warning``, and ``ok`` are the **compliance alert levels** that
112+
will be triggered if the project's scorecard score meets or exceeds the
113+
corresponding threshold.
114+
- The thresholds must be listed in **strictly descending order**.
115+
116+
How it works:
117+
118+
- If the scorecard score is **9.0 or above**, the alert is **``ok``**.
119+
- If the scorecard score is **7.0 to 8.9**, the alert is **``warning``**.
120+
- If the scorecard score is **below 7.0**, the alert is **``error``**.
121+
122+
You can adjust the threshold values and alert levels to match your organization's
123+
security compliance requirements.
124+
125+
Accepted values for the alert level:
126+
127+
- ``ok``
128+
- ``warning``
129+
- ``error``
130+
94131
App Policies
95132
------------
96133

docs/rest-api.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,31 @@ Data:
518518
"license_clarity_compliance_alert": "warning"
519519
}
520520
521+
.. _rest_api_scorecard_compliance:
522+
523+
Scorecard Compliance
524+
^^^^^^^^^^^^^^^^^^^^
525+
526+
This action returns the **scorecard compliance alert** for a project.
527+
528+
The scorecard compliance alert is a single value (``ok``, ``warning``, or ``error``)
529+
that summarizes the project's **OpenSSF Scorecard security compliance status**,
530+
based on the thresholds defined in the ``policies.yml`` file.
531+
532+
``GET /api/projects/6461408c-726c-4b70-aa7a-c9cc9d1c9685/scorecard_compliance/``
533+
534+
Data:
535+
- ``scorecard_compliance_alert``: The overall scorecard compliance alert
536+
for the project.
537+
538+
Possible values: ``ok``, ``warning``, ``error``.
539+
540+
.. code-block:: json
541+
542+
{
543+
"scorecard_compliance_alert": "warning"
544+
}
545+
521546
Reset
522547
^^^^^
523548

docs/tutorial_license_policies.rst

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,51 @@ The ``license_clarity_compliance_alert`` value (e.g., ``"error"``, ``"warning"``
128128
is computed automatically based on the thresholds you configured and reflects the
129129
overall license clarity status of the scanned codebase.
130130

131+
Scorecard Compliance Thresholds and Alerts
132+
------------------------------------------
133+
134+
ScanCode.io also supports **OpenSSF Scorecard compliance thresholds**, allowing you to enforce
135+
minimum security standards for open source packages in your codebase. This is managed
136+
through the ``scorecard_score_thresholds`` section in your ``policies.yml`` file.
137+
138+
Defining Scorecard Thresholds
139+
-----------------------------
140+
141+
Add a ``scorecard_score_thresholds`` section to your ``policies.yml`` file, for example:
142+
143+
.. code-block:: yaml
144+
145+
scorecard_score_thresholds:
146+
9.0: ok
147+
7.0: warning
148+
0: error
149+
150+
Scorecard Compliance in Results
151+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
152+
153+
When you run a the addon pipeline fetch_scores with scorecard thresholds defined in your
154+
``policies.yml``, the computed scorecard compliance alert is included in the project's
155+
``extra_data`` field.
156+
157+
For example:
158+
159+
.. code-block:: json
160+
161+
"extra_data": {
162+
"md5": "d23df4a4",
163+
"sha1": "3e9b61cc98c",
164+
"size": 3095,
165+
"sha256": "abacfc8bcee59067",
166+
"sha512": "208f6a83c83a4c770b3c0",
167+
"filename": "cuckoo_filter-1.0.6.tar.gz",
168+
"sha1_git": "3fdb0f82ad59",
169+
"scorecard_compliance_alert": "warning"
170+
}
171+
172+
The ``scorecard_compliance_alert`` value (e.g., ``"error"``, ``"warning"``, or ``"ok"``)
173+
is computed automatically based on the thresholds you configured and reflects the
174+
overall security compliance status of the OpenSSF Scorecard scores for packages in the scanned codebase.
175+
131176
Run the ``check-compliance`` command
132177
------------------------------------
133178

scanpipe/api/views.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,22 @@ def license_clarity_compliance(self, request, *args, **kwargs):
497497
clarity_alert = project.get_license_clarity_compliance_alert()
498498
return Response({"license_clarity_compliance_alert": clarity_alert})
499499

500+
@action(detail=True, methods=["get"])
501+
def scorecard_compliance(self, request, *args, **kwargs):
502+
"""
503+
Retrieve the scorecard compliance alert for a project.
504+
505+
This endpoint returns the scorecard compliance alert stored in the
506+
project's extra_data.
507+
508+
Example:
509+
GET /api/projects/{project_id}/scorecard_compliance/
510+
511+
"""
512+
project = self.get_object()
513+
scorecard_alert = project.get_scorecard_compliance_alert()
514+
return Response({"scorecard_compliance_alert": scorecard_alert})
515+
500516

501517
class RunViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
502518
"""Add actions to the Run viewset."""

scanpipe/management/commands/check-compliance.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ def check_compliance(self, fail_level):
7777
clarity_alert = self.project.get_license_clarity_compliance_alert()
7878
has_clarity_issue = clarity_alert not in (None, "ok")
7979

80-
total_issues = count + (1 if has_clarity_issue else 0)
80+
scorecard_alert = self.project.get_scorecard_compliance_alert()
81+
has_scorecard_issue = scorecard_alert not in (None, "ok")
82+
83+
total_issues = (
84+
count + (1 if has_clarity_issue else 0) + (1 if has_scorecard_issue else 0)
85+
)
8186

8287
if total_issues and self.verbosity > 0:
8388
self.stderr.write(f"{total_issues} compliance issues detected.")
@@ -92,6 +97,10 @@ def check_compliance(self, fail_level):
9297
self.stderr.write("[license clarity]")
9398
self.stderr.write(f" > {clarity_alert.upper()}")
9499

100+
if has_scorecard_issue:
101+
self.stderr.write("[scorecard compliance]")
102+
self.stderr.write(f" > {scorecard_alert.upper()}")
103+
95104
return total_issues > 0
96105

97106
def check_vulnerabilities(self):

scanpipe/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,13 @@ def get_license_clarity_compliance_alert(self):
15531553
"""
15541554
return self.extra_data.get("license_clarity_compliance_alert")
15551555

1556+
def get_scorecard_compliance_alert(self):
1557+
"""
1558+
Return the scorecard compliance alert value for the project,
1559+
or None if not set.
1560+
"""
1561+
return self.extra_data.get("scorecard_compliance_alert")
1562+
15561563
def get_license_policy_index(self):
15571564
"""Return the policy license index for this project instance."""
15581565
if policies_dict := self.get_policies_dict():

scanpipe/pipelines/fetch_scores.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from scanpipe.models import DiscoveredPackageScore
2727
from scanpipe.pipelines import Pipeline
28+
from scanpipe.pipes import scorecard_compliance
2829

2930

3031
class FetchScores(Pipeline):
@@ -49,6 +50,7 @@ def steps(cls):
4950
return (
5051
cls.check_scorecode_service_availability,
5152
cls.fetch_packages_scorecode_info,
53+
cls.evaluate_compliance_alerts,
5254
)
5355

5456
def check_scorecode_service_availability(self):
@@ -64,3 +66,7 @@ def fetch_packages_scorecode_info(self):
6466
scorecard_data=scorecard_data,
6567
package=package,
6668
)
69+
70+
def evaluate_compliance_alerts(self):
71+
"""Evaluate scorecard compliance alerts for the project."""
72+
scorecard_compliance.evaluate_scorecard_compliance(self.project)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/aboutcode-org/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
22+
23+
from scanpipe.pipes.compliance_thresholds import get_project_scorecard_thresholds
24+
25+
26+
def evaluate_scorecard_compliance(project):
27+
"""
28+
Evaluate scorecard compliance for all discovered packages in the project.
29+
30+
This function checks OpenSSF Scorecard scores against project-defined
31+
thresholds and determines the worst compliance alert level across all packages.
32+
Updates the project's extra_data with the overall compliance status.
33+
"""
34+
scorecard_policy = get_project_scorecard_thresholds(project)
35+
if not scorecard_policy:
36+
return
37+
38+
worst_alert = None
39+
packages_with_scores = project.discoveredpackages.filter(
40+
scores__scoring_tool="ossf-scorecard"
41+
).distinct()
42+
43+
for package in packages_with_scores:
44+
latest_score = (
45+
package.scores.filter(scoring_tool="ossf-scorecard")
46+
.order_by("-score_date")
47+
.first()
48+
)
49+
50+
if not latest_score or latest_score.score is None:
51+
continue
52+
53+
try:
54+
score = float(latest_score.score)
55+
alert = scorecard_policy.get_alert_for_score(score)
56+
except Exception:
57+
alert = "error"
58+
59+
order = {"ok": 0, "warning": 1, "error": 2}
60+
if worst_alert is None or order[alert] > order.get(worst_alert, -1):
61+
worst_alert = alert
62+
63+
if worst_alert is not None:
64+
project.update_extra_data({"scorecard_compliance_alert": worst_alert})

scanpipe/templates/scanpipe/panels/project_compliance.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% load humanize %}
2-
{% if compliance_alerts or license_clarity_compliance_alert %}
2+
{% if compliance_alerts or license_clarity_compliance_alert or scorecard_compliance_alert %}
33
<div class="column is-half">
44
<nav id="compliance-panel" class="panel is-dark">
55
<p class="panel-heading">
@@ -33,6 +33,20 @@
3333
</span>
3434
</div>
3535
{% endif %}
36+
{% if scorecard_compliance_alert %}
37+
<div class="panel-block">
38+
<span class="pr-1">
39+
Scorecard compliance
40+
</span>
41+
<span class="tag is-rounded ml-1
42+
{% if scorecard_compliance_alert == 'error' %}is-danger
43+
{% elif scorecard_compliance_alert == 'warning' %}is-warning
44+
{% elif scorecard_compliance_alert == 'ok' %}is-success
45+
{% else %}is-light{% endif %}">
46+
{{ scorecard_compliance_alert|title }}
47+
</span>
48+
</div>
49+
{% endif %}
3650
</nav>
3751
</div>
3852
{% endif %}

0 commit comments

Comments
 (0)