From 9a1ffa08225d1d0f6bfce1595593b51a76f5e669 Mon Sep 17 00:00:00 2001 From: NucleonGodX Date: Fri, 1 Aug 2025 14:19:33 +0530 Subject: [PATCH 1/3] Add compliance_thresholds.py and related support files Signed-off-by: NucleonGodX --- ...se_clarity.py => compliance_thresholds.py} | 137 ++++---- scanpipe/pipes/scancode.py | 2 +- .../clarity_sample_thresholds.yml} | 0 .../scorecard_sample_thresholds.yml | 4 + .../tests/pipes/test_compliance_thresholds.py | 299 ++++++++++++++++++ scanpipe/tests/pipes/test_license_clarity.py | 162 ---------- 6 files changed, 375 insertions(+), 229 deletions(-) rename scanpipe/pipes/{license_clarity.py => compliance_thresholds.py} (56%) rename scanpipe/tests/data/{license_clarity/sample_thresholds.yml => compliance-thresholds/clarity_sample_thresholds.yml} (100%) create mode 100644 scanpipe/tests/data/compliance-thresholds/scorecard_sample_thresholds.yml create mode 100644 scanpipe/tests/pipes/test_compliance_thresholds.py delete mode 100644 scanpipe/tests/pipes/test_license_clarity.py diff --git a/scanpipe/pipes/license_clarity.py b/scanpipe/pipes/compliance_thresholds.py similarity index 56% rename from scanpipe/pipes/license_clarity.py rename to scanpipe/pipes/compliance_thresholds.py index fa835fc0eb..2f0bc4d90a 100644 --- a/scanpipe/pipes/license_clarity.py +++ b/scanpipe/pipes/compliance_thresholds.py @@ -21,22 +21,40 @@ # Visit https://github.com/nexB/scancode.io for support and download. """ -License Clarity Thresholds Management +Thresholds Management for License Clarity and Scorecard Compliance This module provides an independent mechanism to read, validate, and evaluate -license clarity score thresholds from policy files. Unlike license policies -which are applied during scan processing, clarity thresholds are evaluated -post-scan during summary generation. +both license clarity and OpenSSF Scorecard score thresholds from policy files. +Unlike license and security policies which are applied during scan processing, +these thresholds are evaluated post-scan during summary generation and compliance +assessment. -The clarity thresholds system uses a simple key-value mapping where: -- Keys are integer threshold values (minimum scores) +The thresholds system uses simple key-value mappings where: +- Keys are numeric threshold values (minimum scores) - Values are compliance alert levels ('ok', 'warning', 'error') +License Clarity Thresholds: +- Keys: integer threshold values (minimum clarity scores, 0-100 scale) +- Represents license information completeness percentage + +Scorecard Compliance Thresholds: +- Keys: numeric threshold values (minimum scorecard scores, 0-10.0 scale) +- Represents OpenSSF security assessment (higher score = better security) + Example policies.yml structure: license_clarity_thresholds: - 80: ok # Scores >= 80 get 'ok' alert + 80: ok # Scores >= 80 get 'ok' alert 50: warning # Scores 50-79 get 'warning' alert + 0: error # Scores below 50 get 'error' alert + +scorecard_score_thresholds: + 9.0: ok # Scores >= 9.0 get 'ok' alert + 7.0: warning # Scores 7.0-8.9 get 'warning' alert + 0: error # Scores below 7.0 get 'error' alert + +Both threshold types follow the same evaluation logic but are tailored to their +specific scoring systems and use cases. """ from pathlib import Path @@ -54,58 +72,50 @@ def load_yaml_content(yaml_content): raise ValidationError(f"Policies file format error: {e}") -class ClarityThresholdsPolicy: - """ - Manages clarity score thresholds and compliance evaluation. +class BaseThresholdsPolicy: + """Base class for managing score thresholds and compliance evaluation.""" - This class reads clarity thresholds from a dictionary, validates them - against threshold configurations and determines compliance alerts based on - clarity scores. - """ + YAML_KEY = None + THRESHOLD_TYPE = float + POLICY_NAME = "thresholds" def __init__(self, threshold_dict): - """Initialize with validated threshold dictionary.""" self.thresholds = self.validate_thresholds(threshold_dict) - @staticmethod - def validate_thresholds(threshold_dict): + def validate_thresholds(self, threshold_dict): if not isinstance(threshold_dict, dict): - raise ValidationError( - "The `license_clarity_thresholds` must be a dictionary" - ) + raise ValidationError(f"The `{self.YAML_KEY}` must be a dictionary") + validated = {} seen = set() for key, value in threshold_dict.items(): try: - threshold = int(key) + threshold = self.THRESHOLD_TYPE(key) except (ValueError, TypeError): - raise ValidationError(f"Threshold keys must be integers, got: {key}") + type_name = "integers" if self.THRESHOLD_TYPE == int else "numbers" + raise ValidationError(f"Threshold keys must be {type_name}, got: {key}") + if threshold in seen: raise ValidationError(f"Duplicate threshold key: {threshold}") seen.add(threshold) + if value not in ["ok", "warning", "error"]: raise ValidationError( - f"Compliance alert must be one of 'ok', 'warning', 'error', " - f"got: {value}" + f"Compliance alert must be one of 'ok', 'warning', 'error', got: {value}" ) validated[threshold] = value + sorted_keys = sorted(validated.keys(), reverse=True) if list(validated.keys()) != sorted_keys: raise ValidationError("Thresholds must be strictly descending") + return validated def get_alert_for_score(self, score): - """ - Determine compliance alert level for a given clarity score - - Returns: - str: Compliance alert level ('ok', 'warning', 'error') - - """ + """Determine compliance alert level for a given score.""" if score is None: return "error" - # Find the highest threshold that the score meets or exceeds applicable_thresholds = [t for t in self.thresholds if score >= t] if not applicable_thresholds: return "error" @@ -113,66 +123,49 @@ def get_alert_for_score(self, score): max_threshold = max(applicable_thresholds) return self.thresholds[max_threshold] - def get_thresholds_summary(self): - """ - Get a summary of configured thresholds for reporting - - Returns: - dict: Summary of thresholds and their alert levels - """ - return dict(sorted(self.thresholds.items(), reverse=True)) +# Specific implementations +class ClarityThresholdsPolicy(BaseThresholdsPolicy): + YAML_KEY = "license_clarity_thresholds" + THRESHOLD_TYPE = int + POLICY_NAME = "license clarity thresholds" -def load_clarity_thresholds_from_yaml(yaml_content): - """ - Load clarity thresholds from YAML content. +class ScorecardThresholdsPolicy(BaseThresholdsPolicy): + YAML_KEY = "scorecard_score_thresholds" + THRESHOLD_TYPE = float + POLICY_NAME = "scorecard score thresholds" - Returns: - ClarityThresholdsPolicy: Configured policy object - """ +def load_thresholds_from_yaml(yaml_content, policy_class): + """Generic function to load thresholds from YAML.""" data = load_yaml_content(yaml_content) if not isinstance(data, dict): raise ValidationError("YAML content must be a dictionary.") - if "license_clarity_thresholds" not in data: + if policy_class.YAML_KEY not in data: raise ValidationError( - "Missing 'license_clarity_thresholds' key in policies file." + f"Missing '{policy_class.YAML_KEY}' key in policies file." ) - return ClarityThresholdsPolicy(data["license_clarity_thresholds"]) - + return policy_class(data[policy_class.YAML_KEY]) -def load_clarity_thresholds_from_file(file_path): - """ - Load clarity thresholds from a YAML file. - Returns: - ClarityThresholdsPolicy: Configured policy object or None if file not found - - """ +def load_thresholds_from_file(file_path, policy_class): + """Generic function to load thresholds from file.""" file_path = Path(file_path) - if not file_path.exists(): return try: yaml_content = file_path.read_text(encoding="utf-8") - return load_clarity_thresholds_from_yaml(yaml_content) + return load_thresholds_from_yaml(yaml_content, policy_class) except (OSError, UnicodeDecodeError) as e: raise ValidationError(f"Error reading file {file_path}: {e}") def get_project_clarity_thresholds(project): - """ - Get clarity thresholds for a project using the unified policy loading logic. - - Returns: - ClarityThresholdsPolicy or None: Policy object if thresholds are configured - - """ policies_dict = project.get_policies_dict() if not policies_dict: return @@ -182,3 +175,15 @@ def get_project_clarity_thresholds(project): return return ClarityThresholdsPolicy(clarity_thresholds) + + +def get_project_scorecard_thresholds(project): + policies_dict = project.get_policies_dict() + if not policies_dict: + return + + scorecard_thresholds = policies_dict.get("scorecard_score_thresholds") + if not scorecard_thresholds: + return + + return ScorecardThresholdsPolicy(scorecard_thresholds) diff --git a/scanpipe/pipes/scancode.py b/scanpipe/pipes/scancode.py index 93b20e7710..81ce29c13a 100644 --- a/scanpipe/pipes/scancode.py +++ b/scanpipe/pipes/scancode.py @@ -60,7 +60,7 @@ from scanpipe.models import DiscoveredDependency from scanpipe.models import DiscoveredPackage from scanpipe.pipes import flag -from scanpipe.pipes.license_clarity import get_project_clarity_thresholds +from scanpipe.pipes.compliance_thresholds import get_project_clarity_thresholds logger = logging.getLogger("scanpipe.pipes") diff --git a/scanpipe/tests/data/license_clarity/sample_thresholds.yml b/scanpipe/tests/data/compliance-thresholds/clarity_sample_thresholds.yml similarity index 100% rename from scanpipe/tests/data/license_clarity/sample_thresholds.yml rename to scanpipe/tests/data/compliance-thresholds/clarity_sample_thresholds.yml diff --git a/scanpipe/tests/data/compliance-thresholds/scorecard_sample_thresholds.yml b/scanpipe/tests/data/compliance-thresholds/scorecard_sample_thresholds.yml new file mode 100644 index 0000000000..2afb5153e6 --- /dev/null +++ b/scanpipe/tests/data/compliance-thresholds/scorecard_sample_thresholds.yml @@ -0,0 +1,4 @@ +scorecard_score_thresholds: + 9: ok + 7: warning + 0: error \ No newline at end of file diff --git a/scanpipe/tests/pipes/test_compliance_thresholds.py b/scanpipe/tests/pipes/test_compliance_thresholds.py new file mode 100644 index 0000000000..5ab851dd26 --- /dev/null +++ b/scanpipe/tests/pipes/test_compliance_thresholds.py @@ -0,0 +1,299 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# http://nexb.com and https://github.com/nexB/scancode.io +# The ScanCode.io software is licensed under the Apache License version 2.0. +# Data generated with ScanCode.io is provided as-is without warranties. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# +# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode.io should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# +# ScanCode.io is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode.io for support and download. + +from pathlib import Path + +from django.core.exceptions import ValidationError +from django.test import TestCase + +from scanpipe.pipes.compliance_thresholds import ClarityThresholdsPolicy +from scanpipe.pipes.compliance_thresholds import ScorecardThresholdsPolicy +from scanpipe.pipes.compliance_thresholds import load_thresholds_from_file +from scanpipe.pipes.compliance_thresholds import load_thresholds_from_yaml + + +class ClarityThresholdsPolicyTest(TestCase): + """Test ClarityThresholdsPolicy class functionality.""" + + data = Path(__file__).parent.parent / "data" + + def test_valid_thresholds_initialization(self): + thresholds = {80: "ok", 50: "warning", 20: "error"} + policy = ClarityThresholdsPolicy(thresholds) + self.assertEqual(policy.thresholds, thresholds) + + def test_string_keys_converted_to_integers(self): + thresholds = {"80": "ok", "50": "warning"} + policy = ClarityThresholdsPolicy(thresholds) + expected = {80: "ok", 50: "warning"} + self.assertEqual(policy.thresholds, expected) + + def test_invalid_threshold_key_raises_error(self): + with self.assertRaises(ValidationError) as cm: + ClarityThresholdsPolicy({"invalid": "ok"}) + self.assertIn("must be integers", str(cm.exception)) + + def test_invalid_alert_value_raises_error(self): + with self.assertRaises(ValidationError) as cm: + ClarityThresholdsPolicy({80: "invalid"}) + self.assertIn("must be one of 'ok', 'warning', 'error'", str(cm.exception)) + + def test_non_dict_input_raises_error(self): + with self.assertRaises(ValidationError) as cm: + ClarityThresholdsPolicy([80, 50]) + self.assertIn("must be a dictionary", str(cm.exception)) + + def test_duplicate_threshold_keys_raise_error(self): + with self.assertRaises(ValidationError) as cm: + ClarityThresholdsPolicy({80: "ok", "80": "warning"}) + self.assertIn("Duplicate threshold key", str(cm.exception)) + + def test_overlapping_thresholds_wrong_order(self): + with self.assertRaises(ValidationError) as cm: + ClarityThresholdsPolicy({70: "ok", 80: "warning"}) + self.assertIn("Thresholds must be strictly descending", str(cm.exception)) + + def test_float_threshold_keys(self): + thresholds = {80.5: "ok", 50.9: "warning"} + policy = ClarityThresholdsPolicy(thresholds) + expected = {80: "ok", 50: "warning"} + self.assertEqual(policy.thresholds, expected) + + def test_negative_threshold_values(self): + thresholds = {50: "ok", 0: "warning", -10: "error"} + policy = ClarityThresholdsPolicy(thresholds) + self.assertEqual(policy.get_alert_for_score(60), "ok") + self.assertEqual(policy.get_alert_for_score(25), "warning") + self.assertEqual(policy.get_alert_for_score(-5), "error") + self.assertEqual(policy.get_alert_for_score(-20), "error") + + def test_empty_thresholds_dict(self): + policy = ClarityThresholdsPolicy({}) + self.assertEqual(policy.get_alert_for_score(100), "error") + self.assertEqual(policy.get_alert_for_score(50), "error") + self.assertEqual(policy.get_alert_for_score(0), "error") + self.assertEqual(policy.get_alert_for_score(None), "error") + + def test_very_high_threshold_values(self): + thresholds = {150: "ok", 100: "warning"} + policy = ClarityThresholdsPolicy(thresholds) + self.assertEqual(policy.get_alert_for_score(100), "warning") + self.assertEqual(policy.get_alert_for_score(90), "error") + self.assertEqual(policy.get_alert_for_score(50), "error") + self.assertEqual(policy.get_alert_for_score(99), "error") + + # Policy logic via YAML string (mock policies.yml content) + def test_yaml_string_ok_and_warning(self): + yaml_content = """ +license_clarity_thresholds: + 90: ok + 30: warning +""" + policy = load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + self.assertEqual(policy.get_alert_for_score(95), "ok") + self.assertEqual(policy.get_alert_for_score(60), "warning") + self.assertEqual(policy.get_alert_for_score(20), "error") + + def test_yaml_string_single_threshold(self): + yaml_content = """ +license_clarity_thresholds: + 80: ok +""" + policy = load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + self.assertEqual(policy.get_alert_for_score(90), "ok") + self.assertEqual(policy.get_alert_for_score(79), "error") + + def test_yaml_string_invalid_alert(self): + yaml_content = """ +license_clarity_thresholds: + 80: great +""" + with self.assertRaises(ValidationError): + load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + + def test_yaml_string_invalid_key(self): + yaml_content = """ +license_clarity_thresholds: + eighty: ok +""" + with self.assertRaises(ValidationError): + load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + + def test_yaml_string_missing_key(self): + yaml_content = """ +license_policies: + - license_key: mit +""" + with self.assertRaises(ValidationError): + load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + + def test_yaml_string_invalid_yaml(self): + yaml_content = "license_clarity_thresholds: [80, 50" + with self.assertRaises(ValidationError): + load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + + def test_load_from_existing_file(self): + test_file = self.data / "compliance-thresholds" / "clarity_sample_thresholds.yml" + policy = load_thresholds_from_file(test_file, ClarityThresholdsPolicy) + self.assertIsNotNone(policy) + self.assertEqual(policy.get_alert_for_score(95), "ok") + self.assertEqual(policy.get_alert_for_score(75), "warning") + self.assertEqual(policy.get_alert_for_score(50), "error") + + def test_load_from_nonexistent_file(self): + policy = load_thresholds_from_file("/nonexistent/file.yml", ClarityThresholdsPolicy) + self.assertIsNone(policy) + + +class ScorecardThresholdsPolicyTest(TestCase): + """Test ScorecardThresholdsPolicy class functionality.""" + + data = Path(__file__).parent.parent / "data" + + def test_valid_thresholds_initialization(self): + thresholds = {9.0: "ok", 7.0: "warning", 0: "error"} + policy = ScorecardThresholdsPolicy(thresholds) + self.assertEqual(policy.thresholds, thresholds) + + def test_string_keys_converted_to_floats(self): + thresholds = {"9.0": "ok", "7.0": "warning"} + policy = ScorecardThresholdsPolicy(thresholds) + expected = {9.0: "ok", 7.0: "warning"} + self.assertEqual(policy.thresholds, expected) + + def test_invalid_threshold_key_raises_error(self): + with self.assertRaises(ValidationError) as cm: + ScorecardThresholdsPolicy({"invalid": "ok"}) + self.assertIn("must be numbers", str(cm.exception)) + + def test_invalid_alert_value_raises_error(self): + with self.assertRaises(ValidationError) as cm: + ScorecardThresholdsPolicy({9.0: "invalid"}) + self.assertIn("must be one of 'ok', 'warning', 'error'", str(cm.exception)) + + def test_non_dict_input_raises_error(self): + with self.assertRaises(ValidationError) as cm: + ScorecardThresholdsPolicy([9.0, 7.0]) + self.assertIn("must be a dictionary", str(cm.exception)) + + def test_duplicate_threshold_keys_raise_error(self): + with self.assertRaises(ValidationError) as cm: + ScorecardThresholdsPolicy({9.0: "ok", "9.0": "warning"}) + self.assertIn("Duplicate threshold key", str(cm.exception)) + + def test_overlapping_thresholds_wrong_order(self): + with self.assertRaises(ValidationError) as cm: + ScorecardThresholdsPolicy({7.0: "ok", 9.0: "warning"}) + self.assertIn("Thresholds must be strictly descending", str(cm.exception)) + + def test_float_threshold_keys(self): + thresholds = {9.5: "ok", 7.9: "warning"} + policy = ScorecardThresholdsPolicy(thresholds) + expected = {9.5: "ok", 7.9: "warning"} + self.assertEqual(policy.thresholds, expected) + + def test_negative_threshold_values(self): + thresholds = {5.0: "ok", 0: "warning", -1.0: "error"} + policy = ScorecardThresholdsPolicy(thresholds) + self.assertEqual(policy.get_alert_for_score(6.0), "ok") + self.assertEqual(policy.get_alert_for_score(2.5), "warning") + self.assertEqual(policy.get_alert_for_score(-0.5), "error") + self.assertEqual(policy.get_alert_for_score(-2.0), "error") + + def test_empty_thresholds_dict(self): + policy = ScorecardThresholdsPolicy({}) + self.assertEqual(policy.get_alert_for_score(10.0), "error") + self.assertEqual(policy.get_alert_for_score(5.0), "error") + self.assertEqual(policy.get_alert_for_score(0), "error") + self.assertEqual(policy.get_alert_for_score(None), "error") + + def test_very_high_threshold_values(self): + thresholds = {15.0: "ok", 10.0: "warning"} + policy = ScorecardThresholdsPolicy(thresholds) + self.assertEqual(policy.get_alert_for_score(10.0), "warning") + self.assertEqual(policy.get_alert_for_score(9.0), "error") + self.assertEqual(policy.get_alert_for_score(5.0), "error") + self.assertEqual(policy.get_alert_for_score(9.9), "error") + + # Policy logic via YAML string (mock policies.yml content) + def test_yaml_string_ok_and_warning(self): + yaml_content = """ +scorecard_score_thresholds: + 9.0: ok + 7.0: warning + 0: error +""" + policy = load_thresholds_from_yaml(yaml_content, ScorecardThresholdsPolicy) + self.assertEqual(policy.get_alert_for_score(9.5), "ok") + self.assertEqual(policy.get_alert_for_score(8.0), "warning") + self.assertEqual(policy.get_alert_for_score(2.0), "error") + + def test_yaml_string_single_threshold(self): + yaml_content = """ +scorecard_score_thresholds: + 8.0: ok +""" + policy = load_thresholds_from_yaml(yaml_content, ScorecardThresholdsPolicy) + self.assertEqual(policy.get_alert_for_score(9.0), "ok") + self.assertEqual(policy.get_alert_for_score(7.9), "error") + + def test_yaml_string_invalid_alert(self): + yaml_content = """ +scorecard_score_thresholds: + 8.0: great +""" + with self.assertRaises(ValidationError): + load_thresholds_from_yaml(yaml_content, ScorecardThresholdsPolicy) + + def test_yaml_string_invalid_key(self): + yaml_content = """ +scorecard_score_thresholds: + nine: ok +""" + with self.assertRaises(ValidationError): + load_thresholds_from_yaml(yaml_content, ScorecardThresholdsPolicy) + + def test_yaml_string_missing_key(self): + yaml_content = """ +license_policies: + - license_key: mit +""" + with self.assertRaises(ValidationError): + load_thresholds_from_yaml(yaml_content, ScorecardThresholdsPolicy) + + def test_yaml_string_invalid_yaml(self): + yaml_content = "scorecard_score_thresholds: [9.0, 7.0" + with self.assertRaises(ValidationError): + load_thresholds_from_yaml(yaml_content, ScorecardThresholdsPolicy) + + def test_load_from_existing_file(self): + test_file = self.data / "compliance-thresholds" / "scorecard_sample_thresholds.yml" + policy = load_thresholds_from_file(test_file, ScorecardThresholdsPolicy) + self.assertIsNotNone(policy) + self.assertEqual(policy.get_alert_for_score(9.5), "ok") + self.assertEqual(policy.get_alert_for_score(8.0), "warning") + self.assertEqual(policy.get_alert_for_score(5.0), "error") + + def test_load_from_nonexistent_file(self): + policy = load_thresholds_from_file("/nonexistent/file.yml", ScorecardThresholdsPolicy) + self.assertIsNone(policy) + diff --git a/scanpipe/tests/pipes/test_license_clarity.py b/scanpipe/tests/pipes/test_license_clarity.py deleted file mode 100644 index f00723007b..0000000000 --- a/scanpipe/tests/pipes/test_license_clarity.py +++ /dev/null @@ -1,162 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# -# http://nexb.com and https://github.com/nexB/scancode.io -# The ScanCode.io software is licensed under the Apache License version 2.0. -# Data generated with ScanCode.io is provided as-is without warranties. -# ScanCode is a trademark of nexB Inc. -# -# You may not use this software except in compliance with the License. -# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# -# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES -# OR CONDITIONS OF ANY KIND, either express or implied. No content created from -# ScanCode.io should be considered or used as legal advice. Consult an Attorney -# for any legal advice. -# -# ScanCode.io is a free software code scanning tool from nexB Inc. and others. -# Visit https://github.com/nexB/scancode.io for support and download. - -from pathlib import Path - -from django.core.exceptions import ValidationError -from django.test import TestCase - -from scanpipe.pipes.license_clarity import ClarityThresholdsPolicy -from scanpipe.pipes.license_clarity import load_clarity_thresholds_from_file -from scanpipe.pipes.license_clarity import load_clarity_thresholds_from_yaml - - -class ClarityThresholdsPolicyTest(TestCase): - data = Path(__file__).parent.parent / "data" - """Test ClarityThresholdsPolicy class functionality.""" - - def test_valid_thresholds_initialization(self): - thresholds = {80: "ok", 50: "warning", 20: "error"} - policy = ClarityThresholdsPolicy(thresholds) - self.assertEqual(policy.thresholds, thresholds) - - def test_string_keys_converted_to_integers(self): - thresholds = {"80": "ok", "50": "warning"} - policy = ClarityThresholdsPolicy(thresholds) - expected = {80: "ok", 50: "warning"} - self.assertEqual(policy.thresholds, expected) - - def test_invalid_threshold_key_raises_error(self): - with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy({"invalid": "ok"}) - self.assertIn("must be integers", str(cm.exception)) - - def test_invalid_alert_value_raises_error(self): - with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy({80: "invalid"}) - self.assertIn("must be one of 'ok', 'warning', 'error'", str(cm.exception)) - - def test_non_dict_input_raises_error(self): - with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy([80, 50]) - self.assertIn("must be a dictionary", str(cm.exception)) - - def test_duplicate_threshold_keys_raise_error(self): - with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy({80: "ok", "80": "warning"}) - self.assertIn("Duplicate threshold key", str(cm.exception)) - - def test_overlapping_thresholds_wrong_order(self): - with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy({70: "ok", 80: "warning"}) - self.assertIn("Thresholds must be strictly descending", str(cm.exception)) - - def test_float_threshold_keys(self): - thresholds = {80.5: "ok", 50.9: "warning"} - policy = ClarityThresholdsPolicy(thresholds) - expected = {80: "ok", 50: "warning"} - self.assertEqual(policy.thresholds, expected) - - def test_negative_threshold_values(self): - thresholds = {50: "ok", 0: "warning", -10: "error"} - policy = ClarityThresholdsPolicy(thresholds) - self.assertEqual(policy.get_alert_for_score(60), "ok") - self.assertEqual(policy.get_alert_for_score(25), "warning") - self.assertEqual(policy.get_alert_for_score(-5), "error") - self.assertEqual(policy.get_alert_for_score(-20), "error") - - def test_empty_thresholds_dict(self): - policy = ClarityThresholdsPolicy({}) - self.assertEqual(policy.get_alert_for_score(100), "error") - self.assertEqual(policy.get_alert_for_score(50), "error") - self.assertEqual(policy.get_alert_for_score(0), "error") - self.assertEqual(policy.get_alert_for_score(None), "error") - - def test_very_high_threshold_values(self): - thresholds = {150: "ok", 100: "warning"} - policy = ClarityThresholdsPolicy(thresholds) - self.assertEqual(policy.get_alert_for_score(100), "warning") - self.assertEqual(policy.get_alert_for_score(90), "error") - self.assertEqual(policy.get_alert_for_score(50), "error") - self.assertEqual(policy.get_alert_for_score(99), "error") - - # Policy logic via YAML string (mock policies.yml content) - def test_yaml_string_ok_and_warning(self): - yaml_content = """ -license_clarity_thresholds: - 90: ok - 30: warning -""" - policy = load_clarity_thresholds_from_yaml(yaml_content) - self.assertEqual(policy.get_alert_for_score(95), "ok") - self.assertEqual(policy.get_alert_for_score(60), "warning") - self.assertEqual(policy.get_alert_for_score(20), "error") - - def test_yaml_string_single_threshold(self): - yaml_content = """ -license_clarity_thresholds: - 80: ok -""" - policy = load_clarity_thresholds_from_yaml(yaml_content) - self.assertEqual(policy.get_alert_for_score(90), "ok") - self.assertEqual(policy.get_alert_for_score(79), "error") - - def test_yaml_string_invalid_alert(self): - yaml_content = """ -license_clarity_thresholds: - 80: great -""" - with self.assertRaises(ValidationError): - load_clarity_thresholds_from_yaml(yaml_content) - - def test_yaml_string_invalid_key(self): - yaml_content = """ -license_clarity_thresholds: - eighty: ok -""" - with self.assertRaises(ValidationError): - load_clarity_thresholds_from_yaml(yaml_content) - - def test_yaml_string_missing_key(self): - yaml_content = """ -license_policies: - - license_key: mit -""" - with self.assertRaises(ValidationError): - load_clarity_thresholds_from_yaml(yaml_content) - - def test_yaml_string_invalid_yaml(self): - yaml_content = "license_clarity_thresholds: [80, 50" - with self.assertRaises(ValidationError): - load_clarity_thresholds_from_yaml(yaml_content) - - def test_load_from_existing_file(self): - test_file = self.data / "license_clarity" / "sample_thresholds.yml" - policy = load_clarity_thresholds_from_file(test_file) - self.assertIsNotNone(policy) - self.assertEqual(policy.get_alert_for_score(95), "ok") - self.assertEqual(policy.get_alert_for_score(75), "warning") - self.assertEqual(policy.get_alert_for_score(50), "error") - - def test_load_from_nonexistent_file(self): - policy = load_clarity_thresholds_from_file("/nonexistent/file.yml") - self.assertIsNone(policy) From f88a57d4b5e3eb7825e34184e76825f999d7b320 Mon Sep 17 00:00:00 2001 From: NucleonGodX Date: Fri, 1 Aug 2025 14:30:48 +0530 Subject: [PATCH 2/3] fix tests Signed-off-by: NucleonGodX --- scanpipe/pipes/compliance_thresholds.py | 11 ++++++---- .../tests/pipes/test_compliance_thresholds.py | 21 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/scanpipe/pipes/compliance_thresholds.py b/scanpipe/pipes/compliance_thresholds.py index 2f0bc4d90a..7c33066a7a 100644 --- a/scanpipe/pipes/compliance_thresholds.py +++ b/scanpipe/pipes/compliance_thresholds.py @@ -92,7 +92,9 @@ def validate_thresholds(self, threshold_dict): try: threshold = self.THRESHOLD_TYPE(key) except (ValueError, TypeError): - type_name = "integers" if self.THRESHOLD_TYPE == int else "numbers" + type_name = ( + "integers" if issubclass(self.THRESHOLD_TYPE, int) else "numbers" + ) raise ValidationError(f"Threshold keys must be {type_name}, got: {key}") if threshold in seen: @@ -101,7 +103,8 @@ def validate_thresholds(self, threshold_dict): if value not in ["ok", "warning", "error"]: raise ValidationError( - f"Compliance alert must be one of 'ok', 'warning', 'error', got: {value}" + f"Compliance alert must be one of 'ok', 'warning', 'error', " + f"got: {value}" ) validated[threshold] = value @@ -138,7 +141,7 @@ class ScorecardThresholdsPolicy(BaseThresholdsPolicy): def load_thresholds_from_yaml(yaml_content, policy_class): - """Generic function to load thresholds from YAML.""" + """Load thresholds from YAML.""" data = load_yaml_content(yaml_content) if not isinstance(data, dict): @@ -153,7 +156,7 @@ def load_thresholds_from_yaml(yaml_content, policy_class): def load_thresholds_from_file(file_path, policy_class): - """Generic function to load thresholds from file.""" + """Load thresholds from file.""" file_path = Path(file_path) if not file_path.exists(): return diff --git a/scanpipe/tests/pipes/test_compliance_thresholds.py b/scanpipe/tests/pipes/test_compliance_thresholds.py index 5ab851dd26..38d942019b 100644 --- a/scanpipe/tests/pipes/test_compliance_thresholds.py +++ b/scanpipe/tests/pipes/test_compliance_thresholds.py @@ -33,7 +33,7 @@ class ClarityThresholdsPolicyTest(TestCase): """Test ClarityThresholdsPolicy class functionality.""" - + data = Path(__file__).parent.parent / "data" def test_valid_thresholds_initialization(self): @@ -152,7 +152,9 @@ def test_yaml_string_invalid_yaml(self): load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) def test_load_from_existing_file(self): - test_file = self.data / "compliance-thresholds" / "clarity_sample_thresholds.yml" + test_file = ( + self.data / "compliance-thresholds" / "clarity_sample_thresholds.yml" + ) policy = load_thresholds_from_file(test_file, ClarityThresholdsPolicy) self.assertIsNotNone(policy) self.assertEqual(policy.get_alert_for_score(95), "ok") @@ -160,13 +162,15 @@ def test_load_from_existing_file(self): self.assertEqual(policy.get_alert_for_score(50), "error") def test_load_from_nonexistent_file(self): - policy = load_thresholds_from_file("/nonexistent/file.yml", ClarityThresholdsPolicy) + policy = load_thresholds_from_file( + "/nonexistent/file.yml", ClarityThresholdsPolicy + ) self.assertIsNone(policy) class ScorecardThresholdsPolicyTest(TestCase): """Test ScorecardThresholdsPolicy class functionality.""" - + data = Path(__file__).parent.parent / "data" def test_valid_thresholds_initialization(self): @@ -286,7 +290,9 @@ def test_yaml_string_invalid_yaml(self): load_thresholds_from_yaml(yaml_content, ScorecardThresholdsPolicy) def test_load_from_existing_file(self): - test_file = self.data / "compliance-thresholds" / "scorecard_sample_thresholds.yml" + test_file = ( + self.data / "compliance-thresholds" / "scorecard_sample_thresholds.yml" + ) policy = load_thresholds_from_file(test_file, ScorecardThresholdsPolicy) self.assertIsNotNone(policy) self.assertEqual(policy.get_alert_for_score(9.5), "ok") @@ -294,6 +300,7 @@ def test_load_from_existing_file(self): self.assertEqual(policy.get_alert_for_score(5.0), "error") def test_load_from_nonexistent_file(self): - policy = load_thresholds_from_file("/nonexistent/file.yml", ScorecardThresholdsPolicy) + policy = load_thresholds_from_file( + "/nonexistent/file.yml", ScorecardThresholdsPolicy + ) self.assertIsNone(policy) - From d7dfabaae6b57b4ac8b1316b7ef76b7499e3daa0 Mon Sep 17 00:00:00 2001 From: NucleonGodX Date: Fri, 1 Aug 2025 18:07:57 +0530 Subject: [PATCH 3/3] change ClarityThresholdsPolicy to LicenseClarityThresholdsPolicy Signed-off-by: NucleonGodX --- scanpipe/pipes/compliance_thresholds.py | 4 +- .../tests/pipes/test_compliance_thresholds.py | 44 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/scanpipe/pipes/compliance_thresholds.py b/scanpipe/pipes/compliance_thresholds.py index 7c33066a7a..2603c55730 100644 --- a/scanpipe/pipes/compliance_thresholds.py +++ b/scanpipe/pipes/compliance_thresholds.py @@ -128,7 +128,7 @@ def get_alert_for_score(self, score): # Specific implementations -class ClarityThresholdsPolicy(BaseThresholdsPolicy): +class LicenseClarityThresholdsPolicy(BaseThresholdsPolicy): YAML_KEY = "license_clarity_thresholds" THRESHOLD_TYPE = int POLICY_NAME = "license clarity thresholds" @@ -177,7 +177,7 @@ def get_project_clarity_thresholds(project): if not clarity_thresholds: return - return ClarityThresholdsPolicy(clarity_thresholds) + return LicenseClarityThresholdsPolicy(clarity_thresholds) def get_project_scorecard_thresholds(project): diff --git a/scanpipe/tests/pipes/test_compliance_thresholds.py b/scanpipe/tests/pipes/test_compliance_thresholds.py index 38d942019b..a225663b97 100644 --- a/scanpipe/tests/pipes/test_compliance_thresholds.py +++ b/scanpipe/tests/pipes/test_compliance_thresholds.py @@ -25,69 +25,69 @@ from django.core.exceptions import ValidationError from django.test import TestCase -from scanpipe.pipes.compliance_thresholds import ClarityThresholdsPolicy +from scanpipe.pipes.compliance_thresholds import LicenseClarityThresholdsPolicy from scanpipe.pipes.compliance_thresholds import ScorecardThresholdsPolicy from scanpipe.pipes.compliance_thresholds import load_thresholds_from_file from scanpipe.pipes.compliance_thresholds import load_thresholds_from_yaml -class ClarityThresholdsPolicyTest(TestCase): - """Test ClarityThresholdsPolicy class functionality.""" +class LicenseClarityThresholdsPolicyTest(TestCase): + """Test LicenseClarityThresholdsPolicy class functionality.""" data = Path(__file__).parent.parent / "data" def test_valid_thresholds_initialization(self): thresholds = {80: "ok", 50: "warning", 20: "error"} - policy = ClarityThresholdsPolicy(thresholds) + policy = LicenseClarityThresholdsPolicy(thresholds) self.assertEqual(policy.thresholds, thresholds) def test_string_keys_converted_to_integers(self): thresholds = {"80": "ok", "50": "warning"} - policy = ClarityThresholdsPolicy(thresholds) + policy = LicenseClarityThresholdsPolicy(thresholds) expected = {80: "ok", 50: "warning"} self.assertEqual(policy.thresholds, expected) def test_invalid_threshold_key_raises_error(self): with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy({"invalid": "ok"}) + LicenseClarityThresholdsPolicy({"invalid": "ok"}) self.assertIn("must be integers", str(cm.exception)) def test_invalid_alert_value_raises_error(self): with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy({80: "invalid"}) + LicenseClarityThresholdsPolicy({80: "invalid"}) self.assertIn("must be one of 'ok', 'warning', 'error'", str(cm.exception)) def test_non_dict_input_raises_error(self): with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy([80, 50]) + LicenseClarityThresholdsPolicy([80, 50]) self.assertIn("must be a dictionary", str(cm.exception)) def test_duplicate_threshold_keys_raise_error(self): with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy({80: "ok", "80": "warning"}) + LicenseClarityThresholdsPolicy({80: "ok", "80": "warning"}) self.assertIn("Duplicate threshold key", str(cm.exception)) def test_overlapping_thresholds_wrong_order(self): with self.assertRaises(ValidationError) as cm: - ClarityThresholdsPolicy({70: "ok", 80: "warning"}) + LicenseClarityThresholdsPolicy({70: "ok", 80: "warning"}) self.assertIn("Thresholds must be strictly descending", str(cm.exception)) def test_float_threshold_keys(self): thresholds = {80.5: "ok", 50.9: "warning"} - policy = ClarityThresholdsPolicy(thresholds) + policy = LicenseClarityThresholdsPolicy(thresholds) expected = {80: "ok", 50: "warning"} self.assertEqual(policy.thresholds, expected) def test_negative_threshold_values(self): thresholds = {50: "ok", 0: "warning", -10: "error"} - policy = ClarityThresholdsPolicy(thresholds) + policy = LicenseClarityThresholdsPolicy(thresholds) self.assertEqual(policy.get_alert_for_score(60), "ok") self.assertEqual(policy.get_alert_for_score(25), "warning") self.assertEqual(policy.get_alert_for_score(-5), "error") self.assertEqual(policy.get_alert_for_score(-20), "error") def test_empty_thresholds_dict(self): - policy = ClarityThresholdsPolicy({}) + policy = LicenseClarityThresholdsPolicy({}) self.assertEqual(policy.get_alert_for_score(100), "error") self.assertEqual(policy.get_alert_for_score(50), "error") self.assertEqual(policy.get_alert_for_score(0), "error") @@ -95,7 +95,7 @@ def test_empty_thresholds_dict(self): def test_very_high_threshold_values(self): thresholds = {150: "ok", 100: "warning"} - policy = ClarityThresholdsPolicy(thresholds) + policy = LicenseClarityThresholdsPolicy(thresholds) self.assertEqual(policy.get_alert_for_score(100), "warning") self.assertEqual(policy.get_alert_for_score(90), "error") self.assertEqual(policy.get_alert_for_score(50), "error") @@ -108,7 +108,7 @@ def test_yaml_string_ok_and_warning(self): 90: ok 30: warning """ - policy = load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + policy = load_thresholds_from_yaml(yaml_content, LicenseClarityThresholdsPolicy) self.assertEqual(policy.get_alert_for_score(95), "ok") self.assertEqual(policy.get_alert_for_score(60), "warning") self.assertEqual(policy.get_alert_for_score(20), "error") @@ -118,7 +118,7 @@ def test_yaml_string_single_threshold(self): license_clarity_thresholds: 80: ok """ - policy = load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + policy = load_thresholds_from_yaml(yaml_content, LicenseClarityThresholdsPolicy) self.assertEqual(policy.get_alert_for_score(90), "ok") self.assertEqual(policy.get_alert_for_score(79), "error") @@ -128,7 +128,7 @@ def test_yaml_string_invalid_alert(self): 80: great """ with self.assertRaises(ValidationError): - load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + load_thresholds_from_yaml(yaml_content, LicenseClarityThresholdsPolicy) def test_yaml_string_invalid_key(self): yaml_content = """ @@ -136,7 +136,7 @@ def test_yaml_string_invalid_key(self): eighty: ok """ with self.assertRaises(ValidationError): - load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + load_thresholds_from_yaml(yaml_content, LicenseClarityThresholdsPolicy) def test_yaml_string_missing_key(self): yaml_content = """ @@ -144,18 +144,18 @@ def test_yaml_string_missing_key(self): - license_key: mit """ with self.assertRaises(ValidationError): - load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + load_thresholds_from_yaml(yaml_content, LicenseClarityThresholdsPolicy) def test_yaml_string_invalid_yaml(self): yaml_content = "license_clarity_thresholds: [80, 50" with self.assertRaises(ValidationError): - load_thresholds_from_yaml(yaml_content, ClarityThresholdsPolicy) + load_thresholds_from_yaml(yaml_content, LicenseClarityThresholdsPolicy) def test_load_from_existing_file(self): test_file = ( self.data / "compliance-thresholds" / "clarity_sample_thresholds.yml" ) - policy = load_thresholds_from_file(test_file, ClarityThresholdsPolicy) + policy = load_thresholds_from_file(test_file, LicenseClarityThresholdsPolicy) self.assertIsNotNone(policy) self.assertEqual(policy.get_alert_for_score(95), "ok") self.assertEqual(policy.get_alert_for_score(75), "warning") @@ -163,7 +163,7 @@ def test_load_from_existing_file(self): def test_load_from_nonexistent_file(self): policy = load_thresholds_from_file( - "/nonexistent/file.yml", ClarityThresholdsPolicy + "/nonexistent/file.yml", LicenseClarityThresholdsPolicy ) self.assertIsNone(policy)