Skip to content

Commit 44d6be9

Browse files
authored
Refactor a common threshold mechanism for both license clarity and scorecard score. (#1799)
Signed-off-by: NucleonGodX <[email protected]>
1 parent d0f277e commit 44d6be9

File tree

6 files changed

+384
-228
lines changed

6 files changed

+384
-228
lines changed

scanpipe/pipes/license_clarity.py renamed to scanpipe/pipes/compliance_thresholds.py

Lines changed: 73 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,40 @@
2121
# Visit https://github.com/nexB/scancode.io for support and download.
2222

2323
"""
24-
License Clarity Thresholds Management
24+
Thresholds Management for License Clarity and Scorecard Compliance
2525
2626
This module provides an independent mechanism to read, validate, and evaluate
27-
license clarity score thresholds from policy files. Unlike license policies
28-
which are applied during scan processing, clarity thresholds are evaluated
29-
post-scan during summary generation.
27+
both license clarity and OpenSSF Scorecard score thresholds from policy files.
28+
Unlike license and security policies which are applied during scan processing,
29+
these thresholds are evaluated post-scan during summary generation and compliance
30+
assessment.
3031
31-
The clarity thresholds system uses a simple key-value mapping where:
32-
- Keys are integer threshold values (minimum scores)
32+
The thresholds system uses simple key-value mappings where:
33+
- Keys are numeric threshold values (minimum scores)
3334
- Values are compliance alert levels ('ok', 'warning', 'error')
3435
36+
License Clarity Thresholds:
37+
- Keys: integer threshold values (minimum clarity scores, 0-100 scale)
38+
- Represents license information completeness percentage
39+
40+
Scorecard Compliance Thresholds:
41+
- Keys: numeric threshold values (minimum scorecard scores, 0-10.0 scale)
42+
- Represents OpenSSF security assessment (higher score = better security)
43+
3544
Example policies.yml structure:
3645
3746
license_clarity_thresholds:
38-
80: ok # Scores >= 80 get 'ok' alert
47+
80: ok # Scores >= 80 get 'ok' alert
3948
50: warning # Scores 50-79 get 'warning' alert
49+
0: error # Scores below 50 get 'error' alert
50+
51+
scorecard_score_thresholds:
52+
9.0: ok # Scores >= 9.0 get 'ok' alert
53+
7.0: warning # Scores 7.0-8.9 get 'warning' alert
54+
0: error # Scores below 7.0 get 'error' alert
55+
56+
Both threshold types follow the same evaluation logic but are tailored to their
57+
specific scoring systems and use cases.
4058
"""
4159

4260
from pathlib import Path
@@ -54,125 +72,103 @@ def load_yaml_content(yaml_content):
5472
raise ValidationError(f"Policies file format error: {e}")
5573

5674

57-
class ClarityThresholdsPolicy:
58-
"""
59-
Manages clarity score thresholds and compliance evaluation.
75+
class BaseThresholdsPolicy:
76+
"""Base class for managing score thresholds and compliance evaluation."""
6077

61-
This class reads clarity thresholds from a dictionary, validates them
62-
against threshold configurations and determines compliance alerts based on
63-
clarity scores.
64-
"""
78+
YAML_KEY = None
79+
THRESHOLD_TYPE = float
80+
POLICY_NAME = "thresholds"
6581

6682
def __init__(self, threshold_dict):
67-
"""Initialize with validated threshold dictionary."""
6883
self.thresholds = self.validate_thresholds(threshold_dict)
6984

70-
@staticmethod
71-
def validate_thresholds(threshold_dict):
85+
def validate_thresholds(self, threshold_dict):
7286
if not isinstance(threshold_dict, dict):
73-
raise ValidationError(
74-
"The `license_clarity_thresholds` must be a dictionary"
75-
)
87+
raise ValidationError(f"The `{self.YAML_KEY}` must be a dictionary")
88+
7689
validated = {}
7790
seen = set()
7891
for key, value in threshold_dict.items():
7992
try:
80-
threshold = int(key)
93+
threshold = self.THRESHOLD_TYPE(key)
8194
except (ValueError, TypeError):
82-
raise ValidationError(f"Threshold keys must be integers, got: {key}")
95+
type_name = (
96+
"integers" if issubclass(self.THRESHOLD_TYPE, int) else "numbers"
97+
)
98+
raise ValidationError(f"Threshold keys must be {type_name}, got: {key}")
99+
83100
if threshold in seen:
84101
raise ValidationError(f"Duplicate threshold key: {threshold}")
85102
seen.add(threshold)
103+
86104
if value not in ["ok", "warning", "error"]:
87105
raise ValidationError(
88106
f"Compliance alert must be one of 'ok', 'warning', 'error', "
89107
f"got: {value}"
90108
)
91109
validated[threshold] = value
110+
92111
sorted_keys = sorted(validated.keys(), reverse=True)
93112
if list(validated.keys()) != sorted_keys:
94113
raise ValidationError("Thresholds must be strictly descending")
114+
95115
return validated
96116

97117
def get_alert_for_score(self, score):
98-
"""
99-
Determine compliance alert level for a given clarity score
100-
101-
Returns:
102-
str: Compliance alert level ('ok', 'warning', 'error')
103-
104-
"""
118+
"""Determine compliance alert level for a given score."""
105119
if score is None:
106120
return "error"
107121

108-
# Find the highest threshold that the score meets or exceeds
109122
applicable_thresholds = [t for t in self.thresholds if score >= t]
110123
if not applicable_thresholds:
111124
return "error"
112125

113126
max_threshold = max(applicable_thresholds)
114127
return self.thresholds[max_threshold]
115128

116-
def get_thresholds_summary(self):
117-
"""
118-
Get a summary of configured thresholds for reporting
119-
120-
Returns:
121-
dict: Summary of thresholds and their alert levels
122129

123-
"""
124-
return dict(sorted(self.thresholds.items(), reverse=True))
130+
# Specific implementations
131+
class LicenseClarityThresholdsPolicy(BaseThresholdsPolicy):
132+
YAML_KEY = "license_clarity_thresholds"
133+
THRESHOLD_TYPE = int
134+
POLICY_NAME = "license clarity thresholds"
125135

126136

127-
def load_clarity_thresholds_from_yaml(yaml_content):
128-
"""
129-
Load clarity thresholds from YAML content.
137+
class ScorecardThresholdsPolicy(BaseThresholdsPolicy):
138+
YAML_KEY = "scorecard_score_thresholds"
139+
THRESHOLD_TYPE = float
140+
POLICY_NAME = "scorecard score thresholds"
130141

131-
Returns:
132-
ClarityThresholdsPolicy: Configured policy object
133142

134-
"""
143+
def load_thresholds_from_yaml(yaml_content, policy_class):
144+
"""Load thresholds from YAML."""
135145
data = load_yaml_content(yaml_content)
136146

137147
if not isinstance(data, dict):
138148
raise ValidationError("YAML content must be a dictionary.")
139149

140-
if "license_clarity_thresholds" not in data:
150+
if policy_class.YAML_KEY not in data:
141151
raise ValidationError(
142-
"Missing 'license_clarity_thresholds' key in policies file."
152+
f"Missing '{policy_class.YAML_KEY}' key in policies file."
143153
)
144154

145-
return ClarityThresholdsPolicy(data["license_clarity_thresholds"])
155+
return policy_class(data[policy_class.YAML_KEY])
146156

147157

148-
def load_clarity_thresholds_from_file(file_path):
149-
"""
150-
Load clarity thresholds from a YAML file.
151-
152-
Returns:
153-
ClarityThresholdsPolicy: Configured policy object or None if file not found
154-
155-
"""
158+
def load_thresholds_from_file(file_path, policy_class):
159+
"""Load thresholds from file."""
156160
file_path = Path(file_path)
157-
158161
if not file_path.exists():
159162
return
160163

161164
try:
162165
yaml_content = file_path.read_text(encoding="utf-8")
163-
return load_clarity_thresholds_from_yaml(yaml_content)
166+
return load_thresholds_from_yaml(yaml_content, policy_class)
164167
except (OSError, UnicodeDecodeError) as e:
165168
raise ValidationError(f"Error reading file {file_path}: {e}")
166169

167170

168171
def get_project_clarity_thresholds(project):
169-
"""
170-
Get clarity thresholds for a project using the unified policy loading logic.
171-
172-
Returns:
173-
ClarityThresholdsPolicy or None: Policy object if thresholds are configured
174-
175-
"""
176172
policies_dict = project.get_policies_dict()
177173
if not policies_dict:
178174
return
@@ -181,4 +177,16 @@ def get_project_clarity_thresholds(project):
181177
if not clarity_thresholds:
182178
return
183179

184-
return ClarityThresholdsPolicy(clarity_thresholds)
180+
return LicenseClarityThresholdsPolicy(clarity_thresholds)
181+
182+
183+
def get_project_scorecard_thresholds(project):
184+
policies_dict = project.get_policies_dict()
185+
if not policies_dict:
186+
return
187+
188+
scorecard_thresholds = policies_dict.get("scorecard_score_thresholds")
189+
if not scorecard_thresholds:
190+
return
191+
192+
return ScorecardThresholdsPolicy(scorecard_thresholds)

scanpipe/pipes/scancode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
from scanpipe.models import DiscoveredDependency
6161
from scanpipe.models import DiscoveredPackage
6262
from scanpipe.pipes import flag
63-
from scanpipe.pipes.license_clarity import get_project_clarity_thresholds
63+
from scanpipe.pipes.compliance_thresholds import get_project_clarity_thresholds
6464

6565
logger = logging.getLogger("scanpipe.pipes")
6666

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
scorecard_score_thresholds:
2+
9: ok
3+
7: warning
4+
0: error

0 commit comments

Comments
 (0)