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
2626This 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+
3544Example policies.yml structure:
3645
3746license_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
4260from 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
168171def 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 )
0 commit comments