Skip to content

Commit a356e52

Browse files
committed
initial push to add clarity-based compliance support
Signed-off-by: NucleonGodX <[email protected]>
1 parent d9875ff commit a356e52

File tree

5 files changed

+103
-11
lines changed

5 files changed

+103
-11
lines changed

scanpipe/apps.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
from scanpipe.policies import load_policies_file
4141
from scanpipe.policies import make_license_policy_index
42+
from scanpipe.policies import make_clarity_policy_index
4243

4344
try:
4445
from importlib import metadata as importlib_metadata
@@ -218,7 +219,7 @@ def get_scancode_licenses(self):
218219

219220
def set_policies(self):
220221
"""
221-
Compute and sets the `license_policies` on the app instance.
222+
Compute and sets the `license_policies` and `clarity_policies` on the app instance.
222223
223224
If the policies file is available but not formatted properly or doesn't
224225
include the proper content, we want to raise an exception while the app
@@ -233,8 +234,7 @@ def set_policies(self):
233234
policies = load_policies_file(policies_file)
234235
logger.debug(style.SUCCESS(f"Loaded policies from {policies_file}"))
235236
self.license_policies_index = make_license_policy_index(policies)
236-
else:
237-
logger.debug(style.WARNING("Policies file not found."))
237+
self.clarity_policies_index = make_clarity_policy_index(policies)
238238

239239
def sync_runs_and_jobs(self):
240240
"""Synchronize ``QUEUED`` and ``RUNNING`` Run with their related Jobs."""

scanpipe/models.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,30 +1503,50 @@ def get_policy_index(self):
15031503
The policies are loaded from the following locations in that order:
15041504
1. the project local settings
15051505
2. the "policies.yml" file in the project input/ directory
1506-
3. the global app settings license policies
1506+
3. the global app settings license and clarity policies
15071507
"""
15081508
if policies_from_settings := self.get_env("policies"):
15091509
policies_dict = policies_from_settings
15101510
if isinstance(policies_from_settings, str):
15111511
policies_dict = policies.load_policies_yaml(policies_from_settings)
1512-
return policies.make_license_policy_index(policies_dict)
1512+
return {
1513+
'license_policies': policies.make_license_policy_index(policies_dict),
1514+
'clarity_policies': policies.make_clarity_policy_index(policies_dict)
1515+
}
15131516

15141517
elif policies_file := self.get_input_policies_file():
15151518
policies_dict = policies.load_policies_file(policies_file)
1516-
return policies.make_license_policy_index(policies_dict)
1519+
return {
1520+
'license_policies': policies.make_license_policy_index(policies_dict),
1521+
'clarity_policies': policies.make_clarity_policy_index(policies_dict)
1522+
}
15171523

15181524
else:
1519-
return scanpipe_app.license_policies_index
1525+
return {
1526+
'license_policies': scanpipe_app.license_policies_index,
1527+
'clarity_policies': getattr(scanpipe_app, 'clarity_policies_index', [])
1528+
}
15201529

15211530
@cached_property
15221531
def policy_index(self):
1523-
"""Return the cached policy index for this project instance."""
1524-
return self.get_policy_index()
1532+
"""Return the cached license policy index for this project instance"""
1533+
full_policies = self.get_policy_index()
1534+
return full_policies.get('license_policies', {})
1535+
1536+
@cached_property
1537+
def clarity_policy_index(self):
1538+
"""Return clarity policies"""
1539+
full_policies = self.get_policy_index()
1540+
clarity_policies = full_policies.get('clarity_policies', [])
1541+
return clarity_policies
15251542

15261543
@property
15271544
def policies_enabled(self):
1528-
"""Return True if the policies are enabled for this project."""
1529-
return bool(self.policy_index)
1545+
"""Return True if any policies (license or clarity) are enabled for this project."""
1546+
full_policies = self.get_policy_index()
1547+
license_policies = full_policies.get('license_policies', {})
1548+
clarity_policies = full_policies.get('clarity_policies', [])
1549+
return bool(license_policies or clarity_policies)
15301550

15311551

15321552
class GroupingQuerySetMixin:

scanpipe/pipes/compliance.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from scanpipe.models import ComplianceAlertMixin
2727
from scanpipe.pipes import flag
2828
from scanpipe.pipes import scancode
29+
from scanpipe import policies
2930

3031
"""
3132
A common compliance pattern for images is to store known licenses in a /licenses
@@ -123,3 +124,19 @@ def get_project_compliance_alerts(project, fail_level="error"):
123124
}
124125

125126
return project_compliance_alerts
127+
128+
def add_clarity_compliance_to_summary(summary, project):
129+
"""
130+
Add clarity compliance alert to summary data.
131+
Called from make_results_summary function.
132+
"""
133+
134+
clarity_score = summary.get("license_clarity_score", {}).get("score")
135+
if clarity_score is None:
136+
return summary
137+
138+
clarity_policies = project.clarity_policy_index
139+
clarity_compliance = policies.evaluate_clarity_compliance(clarity_score, clarity_policies)
140+
141+
summary["clarity_compliance_alert"] = clarity_compliance
142+
return summary

scanpipe/pipes/scancode.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,7 @@ def make_results_summary(project, scan_results_location):
934934
"""
935935
from scanpipe.api.serializers import CodebaseResourceSerializer
936936
from scanpipe.api.serializers import DiscoveredPackageSerializer
937+
from scanpipe import policies
937938

938939
with open(scan_results_location) as f:
939940
scan_data = json.load(f)
@@ -964,4 +965,10 @@ def make_results_summary(project, scan_results_location):
964965
DiscoveredPackageSerializer(package).data for package in key_files_packages_qs
965966
]
966967

968+
clarity_score = summary.get("license_clarity_score", {}).get("score")
969+
if clarity_score is not None:
970+
clarity_policies = project.clarity_policy_index
971+
clarity_compliance = policies.evaluate_clarity_compliance(clarity_score, clarity_policies)
972+
summary["clarity_compliance_alert"] = clarity_compliance
973+
967974
return summary

scanpipe/policies.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,27 @@ def validate_policies(policies_dict):
5454
"The `license_policies` key is missing from provided policies data."
5555
)
5656

57+
if "clarity_policies" in policies_dict:
58+
clarity_policies = policies_dict["clarity_policies"]
59+
if not isinstance(clarity_policies, list):
60+
raise ValidationError("The `clarity_policies` must be a list.")
61+
62+
for policy in clarity_policies:
63+
if not isinstance(policy, dict):
64+
raise ValidationError("Each clarity policy must be a dictionary.")
65+
if "threshold" not in policy:
66+
raise ValidationError("Each clarity policy must have a 'threshold' field.")
67+
68+
threshold = policy["threshold"]
69+
if isinstance(threshold, str):
70+
try:
71+
policy["threshold"] = float(threshold) if '.' in threshold else int(threshold)
72+
except ValueError:
73+
raise ValidationError(f"Clarity policy 'threshold' must be a valid number. Got: {threshold}")
74+
75+
if not isinstance(policy["threshold"], (int, float)):
76+
raise ValidationError("Clarity policy 'threshold' must be a number.")
77+
5778
return True
5879

5980

@@ -63,3 +84,30 @@ def make_license_policy_index(policies_dict):
6384

6485
license_policies = policies_dict.get("license_policies", [])
6586
return {policy.get("license_key"): policy for policy in license_policies}
87+
88+
89+
def make_clarity_policy_index(policies_dict):
90+
"""Return a list of clarity policies sorted by threshold (descending)."""
91+
if "clarity_policies" not in policies_dict:
92+
return []
93+
94+
clarity_policies = policies_dict.get("clarity_policies", [])
95+
return sorted(clarity_policies, key=lambda p: p.get("threshold", 0), reverse=True)
96+
97+
98+
def evaluate_clarity_compliance(clarity_score, clarity_policies):
99+
"""
100+
Evaluate clarity score against policies and return compliance alert.
101+
Returns the most appropriate compliance alert based on the score.
102+
"""
103+
if not clarity_policies:
104+
return ""
105+
106+
if clarity_score is None:
107+
return "missing"
108+
109+
for policy in clarity_policies:
110+
if clarity_score >= policy.get("threshold", 0):
111+
return policy.get("compliance_alert", "")
112+
113+
return "error"

0 commit comments

Comments
 (0)