From dee13ab0768d6ab81e4ebc5bda9f52c1e443e85c Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Wed, 14 Jan 2026 14:28:00 +0000 Subject: [PATCH 01/10] feat: add project level non-compliance flag to health metrics --- ...rojecthealthmetrics_level_non_compliant.py | 22 +++++++++++++++++++ .../owasp/models/project_health_metrics.py | 5 +++++ 2 files changed, 27 insertions(+) create mode 100644 backend/apps/owasp/migrations/0070_projecthealthmetrics_level_non_compliant.py diff --git a/backend/apps/owasp/migrations/0070_projecthealthmetrics_level_non_compliant.py b/backend/apps/owasp/migrations/0070_projecthealthmetrics_level_non_compliant.py new file mode 100644 index 0000000000..1fe1096215 --- /dev/null +++ b/backend/apps/owasp/migrations/0070_projecthealthmetrics_level_non_compliant.py @@ -0,0 +1,22 @@ +# Generated by Django on 2026-01-14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("owasp", "0069_alter_project_contribution_data_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="projecthealthmetrics", + name="level_non_compliant", + field=models.BooleanField( + default=False, + verbose_name="Is level non-compliant", + help_text="True when local project level differs from official OWASP level.", + ), + ), + ] diff --git a/backend/apps/owasp/models/project_health_metrics.py b/backend/apps/owasp/models/project_health_metrics.py index 36a88c23ea..7e18ed5729 100644 --- a/backend/apps/owasp/models/project_health_metrics.py +++ b/backend/apps/owasp/models/project_health_metrics.py @@ -43,6 +43,11 @@ class Meta: is_leader_requirements_compliant = models.BooleanField( verbose_name="Is leader requirements compliant", default=False ) + level_non_compliant = models.BooleanField( + verbose_name="Is level non-compliant", + default=False, + help_text="True when local project level differs from official OWASP level.", + ) last_released_at = models.DateTimeField(verbose_name="Last released at", blank=True, null=True) last_committed_at = models.DateTimeField( verbose_name="Last committed at", blank=True, null=True From d76cdb1c3f4eb426012b3d475451f1d0324713ce Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Wed, 14 Jan 2026 14:48:47 +0000 Subject: [PATCH 02/10] detect OWASP project level non-compliance --- .../owasp_update_project_level_compliance.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py diff --git a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py new file mode 100644 index 0000000000..36ddbf04cb --- /dev/null +++ b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py @@ -0,0 +1,83 @@ +"""Detect OWASP project level non-compliance using official source of truth.""" + +import re +from decimal import Decimal, InvalidOperation + +import requests +from django.core.management.base import BaseCommand + +from apps.owasp.models.project_health_metrics import ProjectHealthMetrics +from apps.owasp.utils.project_level import map_level + + +LEVELS_URL = ( + "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/project_levels.json" +) + + +def normalize_name(name: str) -> str: + """Normalize project names for comparison.""" + return re.sub(r"[^a-z0-9]+", "", name.lower().replace("owasp", "")) + + +class Command(BaseCommand): + """Update project level non-compliance flag.""" + + help = "Detect and flag OWASP project level non-compliance." + + def handle(self, *args, **options) -> None: + response = requests.get(LEVELS_URL, timeout=15) + response.raise_for_status() + official_data = response.json() + + by_repo: dict[str, Decimal] = {} + by_name: dict[str, Decimal] = {} + + for item in official_data: + raw_level = item.get("level") + try: + level = Decimal(str(raw_level)) + except (InvalidOperation, TypeError, ValueError): + continue + + if repo := item.get("repo"): + by_repo[repo.lower()] = level + + if name := item.get("name"): + by_name[normalize_name(name)] = level + + metrics = ProjectHealthMetrics.objects.select_related("project") + updated_metrics = [] + + for metric in metrics: + project = metric.project + official_level = None + + if project.owasp_url: + slug = project.owasp_url.rstrip("/").split("/")[-1].lower() + official_level = by_repo.get(slug) + + if official_level is None: + official_level = by_name.get(normalize_name(project.name)) + + if official_level is None: + continue + + expected_level = map_level(official_level) + if expected_level is None: + continue + + metric.level_non_compliant = project.level != expected_level + updated_metrics.append(metric) + + if updated_metrics: + ProjectHealthMetrics.bulk_save( + updated_metrics, + fields=["level_non_compliant"], + ) + + self.stdout.write( + self.style.SUCCESS( + f"Updated level compliance for {len(updated_metrics)} projects." + ) + ) From 093a8b7f8798e4e325ccd14df4a4d56b52a3e424 Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Wed, 14 Jan 2026 15:47:05 +0000 Subject: [PATCH 03/10] detect project level non-compliance and penalize health score --- .../owasp_update_project_health_scores.py | 57 +++++++++----- .../owasp_update_project_level_compliance.py | 5 +- ...rojecthealthmetrics_level_non_compliant.py | 1 - ...owasp_update_project_health_scores_test.py | 75 +++++++++++++++++++ 4 files changed, 114 insertions(+), 24 deletions(-) diff --git a/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py b/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py index 44f11a5a41..ccc3c539c1 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py @@ -5,11 +5,19 @@ from apps.owasp.models.project_health_metrics import ProjectHealthMetrics from apps.owasp.models.project_health_requirements import ProjectHealthRequirements +LEVEL_NON_COMPLIANCE_PENALTY = 10.0 + class Command(BaseCommand): + """Update OWASP project health scores.""" + help = "Update OWASP project health scores." def handle(self, *args, **options): + """Recalculate and update project health scores. + + Applies a fixed penalty when a project is marked as level non-compliant. + """ forward_fields = { "age_days": 6.0, "contributors_count": 6.0, @@ -22,6 +30,7 @@ def handle(self, *args, **options): "total_pull_requests_count": 6.0, "total_releases_count": 6.0, } + backward_fields = { "last_commit_days": 6.0, "last_pull_request_days": 6.0, @@ -32,23 +41,30 @@ def handle(self, *args, **options): "unassigned_issues_count": 6.0, } - project_health_metrics = [] - project_health_requirements = { - phr.level: phr for phr in ProjectHealthRequirements.objects.all() - } - for metric in ProjectHealthMetrics.objects.filter( - score__isnull=True, - ).select_related( - "project", + requirements_by_level = {req.level: req for req in ProjectHealthRequirements.objects.all()} + + metrics_to_update = [] + + for metric in ProjectHealthMetrics.objects.filter(score__isnull=True).select_related( + "project" ): - # Calculate the score based on requirements. + requirements = requirements_by_level.get(metric.project.level) + + if not requirements: + self.stdout.write( + self.style.WARNING( + f"Skipping {metric.project.name}: " + f"No requirements found for level {metric.project.level}" + ) + ) + continue + self.stdout.write( self.style.NOTICE(f"Updating score for project: {metric.project.name}") ) - requirements = project_health_requirements[metric.project.level] - score = 0.0 + for field, weight in forward_fields.items(): if int(getattr(metric, field)) >= int(getattr(requirements, field)): score += weight @@ -57,13 +73,16 @@ def handle(self, *args, **options): if int(getattr(metric, field)) <= int(getattr(requirements, field)): score += weight - metric.score = score - project_health_metrics.append(metric) + if metric.level_non_compliant: + score -= LEVEL_NON_COMPLIANCE_PENALTY + + metric.score = max(score, 0.0) + metrics_to_update.append(metric) + + if metrics_to_update: + ProjectHealthMetrics.bulk_save( + metrics_to_update, + fields=["score"], + ) - ProjectHealthMetrics.bulk_save( - project_health_metrics, - fields=[ - "score", - ], - ) self.stdout.write(self.style.SUCCESS("Updated project health scores successfully.")) diff --git a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py index 36ddbf04cb..3c20710dcc 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py @@ -9,7 +9,6 @@ from apps.owasp.models.project_health_metrics import ProjectHealthMetrics from apps.owasp.utils.project_level import map_level - LEVELS_URL = ( "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/project_levels.json" ) @@ -77,7 +76,5 @@ def handle(self, *args, **options) -> None: ) self.stdout.write( - self.style.SUCCESS( - f"Updated level compliance for {len(updated_metrics)} projects." - ) + self.style.SUCCESS(f"Updated level compliance for {len(updated_metrics)} projects.") ) diff --git a/backend/apps/owasp/migrations/0070_projecthealthmetrics_level_non_compliant.py b/backend/apps/owasp/migrations/0070_projecthealthmetrics_level_non_compliant.py index 1fe1096215..e255942644 100644 --- a/backend/apps/owasp/migrations/0070_projecthealthmetrics_level_non_compliant.py +++ b/backend/apps/owasp/migrations/0070_projecthealthmetrics_level_non_compliant.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("owasp", "0069_alter_project_contribution_data_and_more"), ] diff --git a/backend/tests/apps/owasp/management/commands/owasp_update_project_health_scores_test.py b/backend/tests/apps/owasp/management/commands/owasp_update_project_health_scores_test.py index de7862a6dd..2ef89d340c 100644 --- a/backend/tests/apps/owasp/management/commands/owasp_update_project_health_scores_test.py +++ b/backend/tests/apps/owasp/management/commands/owasp_update_project_health_scores_test.py @@ -82,3 +82,78 @@ def test_handle_successful_update(self): assert mock_metric.score == EXPECTED_SCORE assert "Updated project health scores successfully." in self.stdout.getvalue() assert "Updating score for project: Test Project" in self.stdout.getvalue() + + def test_handle_applies_level_non_compliance_penalty(self): + """Test score reduction when project level is non-compliant. + + Ensures that a fixed penalty is applied to the calculated + project health score when the project is marked as + level non-compliant. + """ + mock_metric = MagicMock(spec=ProjectHealthMetrics) + mock_requirements = MagicMock(spec=ProjectHealthRequirements) + + # Minimal values to guarantee a positive base score + mock_metric.age_days = 10 + mock_requirements.age_days = 5 + + mock_metric.contributors_count = 10 + mock_requirements.contributors_count = 5 + + mock_metric.forks_count = 10 + mock_requirements.forks_count = 5 + + mock_metric.last_commit_days = 1 + mock_requirements.last_commit_days = 5 + + mock_metric.last_release_days = 1 + mock_requirements.last_release_days = 5 + + mock_metric.open_issues_count = 1 + mock_requirements.open_issues_count = 5 + + mock_metric.open_pull_requests_count = 1 + mock_requirements.open_pull_requests_count = 5 + + mock_metric.owasp_page_last_update_days = 1 + mock_requirements.owasp_page_last_update_days = 5 + + mock_metric.last_pull_request_days = 1 + mock_requirements.last_pull_request_days = 5 + + mock_metric.recent_releases_count = 10 + mock_requirements.recent_releases_count = 5 + + mock_metric.stars_count = 10 + mock_requirements.stars_count = 5 + + mock_metric.total_pull_requests_count = 10 + mock_requirements.total_pull_requests_count = 5 + + mock_metric.total_releases_count = 10 + mock_requirements.total_releases_count = 5 + + mock_metric.unanswered_issues_count = 1 + mock_requirements.unanswered_issues_count = 5 + + mock_metric.unassigned_issues_count = 1 + mock_requirements.unassigned_issues_count = 5 + + mock_metric.level_non_compliant = True + mock_metric.is_funding_requirements_compliant = True + mock_metric.is_leader_requirements_compliant = True + + mock_metric.project.level = "test_level" + mock_metric.project.name = "Non Compliant Project" + mock_requirements.level = "test_level" + + self.mock_metrics.return_value.select_related.return_value = [mock_metric] + self.mock_requirements.return_value = [mock_requirements] + + with patch("sys.stdout", new=self.stdout): + call_command("owasp_update_project_health_scores") + + self.mock_bulk_save.assert_called_once() + assert mock_metric.score >= 0 + assert mock_metric.level_non_compliant is True + assert "Updating score for project: Non Compliant Project" in self.stdout.getvalue() From 4ff4573404d6bb420fdba4750ebb0d8291e032ed Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Wed, 14 Jan 2026 17:25:21 +0000 Subject: [PATCH 04/10] Add project level compliance and integrate with health scoring --- .../commands/owasp_aggregate_projects.py | 8 +++ .../owasp_update_project_health_scores.py | 27 +++++-- .../owasp_update_project_level_compliance.py | 30 +++++++- backend/apps/owasp/utils/project_level.py | 44 ++++++++++++ .../commands/owasp_aggregate_projects_test.py | 9 +++ ...owasp_update_project_health_scores_test.py | 1 + ...sp_update_project_level_compliance_test.py | 70 +++++++++++++++++++ .../apps/owasp/utils/project_level_test.py | 43 ++++++++++++ 8 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 backend/apps/owasp/utils/project_level.py create mode 100644 backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py create mode 100644 backend/tests/apps/owasp/utils/project_level_test.py diff --git a/backend/apps/owasp/management/commands/owasp_aggregate_projects.py b/backend/apps/owasp/management/commands/owasp_aggregate_projects.py index 2c0b9c6702..f278c38545 100644 --- a/backend/apps/owasp/management/commands/owasp_aggregate_projects.py +++ b/backend/apps/owasp/management/commands/owasp_aggregate_projects.py @@ -1,5 +1,6 @@ """A command to update OWASP projects data.""" +from django.core.management import call_command from django.core.management.base import BaseCommand from apps.owasp.models.project import Project @@ -114,3 +115,10 @@ def handle(self, *_args, **options) -> None: # Bulk save data. Project.bulk_save(projects) + + if offset == 0: + self.stdout.write(self.style.NOTICE("Updating project level compliance...")) + call_command("owasp_update_project_level_compliance") + + self.stdout.write(self.style.NOTICE("Updating project health scores...")) + call_command("owasp_update_project_health_scores") diff --git a/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py b/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py index ccc3c539c1..dba01dc64e 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py @@ -1,4 +1,12 @@ -"""A command to update OWASP project health metrics scores.""" +""" +Update OWASP project health scores. + +This command calculates health scores for OWASP projects +based on defined metrics and requirement thresholds. +Projects marked as level non-compliant receive an +additional score penalty to reflect misalignment with +official OWASP project classification. +""" from django.core.management.base import BaseCommand @@ -9,14 +17,25 @@ class Command(BaseCommand): - """Update OWASP project health scores.""" + """ + Compute and update project health scores. + + Health scores are derived from project metrics and + requirement definitions. Projects that fail level + compliance checks are penalized to ensure score + accuracy reflects governance alignment. + """ help = "Update OWASP project health scores." def handle(self, *args, **options): - """Recalculate and update project health scores. + """ + Calculate and persist project health scores. - Applies a fixed penalty when a project is marked as level non-compliant. + This method iterates over project health metrics, + evaluates each metric against defined requirements, + applies penalties for non-compliant project levels, + and stores the final computed score. """ forward_fields = { "age_days": 6.0, diff --git a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py index 3c20710dcc..c9d3b175b2 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py @@ -1,4 +1,15 @@ -"""Detect OWASP project level non-compliance using official source of truth.""" +""" +Update OWASP project level compliance status. + +This command compares locally stored project levels against +official OWASP project classification data and determines +whether each project is level-compliant. + +Projects whose locally assigned level does not match the +official OWASP classification are flagged as non-compliant. +This compliance flag is later used during health score +calculation to apply penalties where appropriate. +""" import re from decimal import Decimal, InvalidOperation @@ -20,11 +31,26 @@ def normalize_name(name: str) -> str: class Command(BaseCommand): - """Update project level non-compliance flag.""" + """ + Detect and persist OWASP project level compliance. + + This command fetches official OWASP project level data, + maps it to the internal ProjectLevel enum, and updates + project health metrics to indicate whether a project’s + stored level matches the official classification. + """ help = "Detect and flag OWASP project level non-compliance." def handle(self, *args, **options) -> None: + """ + Execute project level compliance detection. + + For each project health metric entry, this method + determines the expected project level based on + official OWASP data and marks the project as + level non-compliant when a mismatch is detected. + """ response = requests.get(LEVELS_URL, timeout=15) response.raise_for_status() official_data = response.json() diff --git a/backend/apps/owasp/utils/project_level.py b/backend/apps/owasp/utils/project_level.py new file mode 100644 index 0000000000..abe5a1019a --- /dev/null +++ b/backend/apps/owasp/utils/project_level.py @@ -0,0 +1,44 @@ +""" +Utilities for mapping OWASP official numeric project levels +to internal ProjectLevel enum values. + +This module acts as a translation layer between the OWASP +project level definitions published upstream and the +Nest internal project classification system. +""" + +from decimal import Decimal, InvalidOperation +from typing import cast + +from apps.owasp.models.enums.project import ProjectLevel + + +_LEVEL_MAP = { + Decimal(4): ProjectLevel.FLAGSHIP, + Decimal("3.5"): ProjectLevel.FLAGSHIP, + Decimal(3): ProjectLevel.PRODUCTION, + Decimal(2): ProjectLevel.INCUBATOR, + Decimal(1): ProjectLevel.LAB, + Decimal(0): ProjectLevel.OTHER, +} + + +def map_level(level: Decimal) -> ProjectLevel | None: + """Map an OWASP official numeric project level to ProjectLevel. + + Args: + level (Decimal): The numeric project level provided by OWASP. + + Returns: + ProjectLevel | None: The mapped ProjectLevel value if valid, + otherwise None for unsupported or invalid levels. + """ + try: + parsed_level = Decimal(str(level)) + except (InvalidOperation, TypeError, ValueError): + return None + + if parsed_level < 0: + return None + + return cast(ProjectLevel | None, _LEVEL_MAP.get(parsed_level)) diff --git a/backend/tests/apps/owasp/management/commands/owasp_aggregate_projects_test.py b/backend/tests/apps/owasp/management/commands/owasp_aggregate_projects_test.py index bdc2612a90..e744300e65 100644 --- a/backend/tests/apps/owasp/management/commands/owasp_aggregate_projects_test.py +++ b/backend/tests/apps/owasp/management/commands/owasp_aggregate_projects_test.py @@ -76,9 +76,18 @@ def exists(self): with ( mock.patch.object(Project, "active_projects", mock_active_projects), mock.patch("builtins.print") as mock_print, + mock.patch( + "apps.owasp.management.commands.owasp_aggregate_projects.call_command" + ) as mock_call_command, ): command.handle(offset=offset) + if offset == 0: + mock_call_command.assert_any_call("owasp_update_project_level_compliance") + mock_call_command.assert_any_call("owasp_update_project_health_scores") + else: + mock_call_command.assert_not_called() + assert mock_bulk_save.called assert mock_print.call_count == projects - offset diff --git a/backend/tests/apps/owasp/management/commands/owasp_update_project_health_scores_test.py b/backend/tests/apps/owasp/management/commands/owasp_update_project_health_scores_test.py index 2ef89d340c..91117d14e5 100644 --- a/backend/tests/apps/owasp/management/commands/owasp_update_project_health_scores_test.py +++ b/backend/tests/apps/owasp/management/commands/owasp_update_project_health_scores_test.py @@ -63,6 +63,7 @@ def test_handle_successful_update(self): mock_metric.project.name = "Test Project" mock_metric.is_funding_requirements_compliant = True mock_metric.is_leader_requirements_compliant = True + mock_metric.level_non_compliant = False self.mock_metrics.return_value.select_related.return_value = [mock_metric] self.mock_requirements.return_value = [mock_requirements] mock_requirements.level = "test_level" diff --git a/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py b/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py new file mode 100644 index 0000000000..8b0ab140e7 --- /dev/null +++ b/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py @@ -0,0 +1,70 @@ +from unittest.mock import MagicMock, patch + +from django.core.management import call_command +from django.test import SimpleTestCase + +from apps.owasp.models.enums.project import ProjectLevel + + +class TestProjectLevelCompliance(SimpleTestCase): + @patch( + "apps.owasp.management.commands.owasp_update_project_level_compliance." + "ProjectHealthMetrics.bulk_save" + ) + @patch( + "apps.owasp.management.commands.owasp_update_project_level_compliance." + "ProjectHealthMetrics.objects" + ) + @patch( + "apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get" + ) + def test_marks_project_as_non_compliant_when_levels_differ( + self, mock_get, mock_objects, mock_bulk_save + ): + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = [ + {"name": "Test Project", "repo": "test-project", "level": "4"} + ] + + metric = MagicMock() + metric.project.name = "Test Project" + metric.project.level = ProjectLevel.PRODUCTION + metric.level_non_compliant = False + + mock_objects.select_related.return_value = [metric] + + call_command("owasp_update_project_level_compliance") + + assert metric.level_non_compliant is True + mock_bulk_save.assert_called_once() + + @patch( + "apps.owasp.management.commands.owasp_update_project_level_compliance." + "ProjectHealthMetrics.bulk_save" + ) + @patch( + "apps.owasp.management.commands.owasp_update_project_level_compliance." + "ProjectHealthMetrics.objects" + ) + @patch( + "apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get" + ) + def test_project_remains_compliant_when_levels_match( + self, mock_get, mock_objects, mock_bulk_save + ): + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = [ + {"name": "Test Project", "repo": "test-project", "level": "3"} + ] + + metric = MagicMock() + metric.project.name = "Test Project" + metric.project.level = ProjectLevel.PRODUCTION + metric.level_non_compliant = False + + mock_objects.select_related.return_value = [metric] + + call_command("owasp_update_project_level_compliance") + + assert metric.level_non_compliant is False + mock_bulk_save.assert_called_once() diff --git a/backend/tests/apps/owasp/utils/project_level_test.py b/backend/tests/apps/owasp/utils/project_level_test.py new file mode 100644 index 0000000000..d1a662b82c --- /dev/null +++ b/backend/tests/apps/owasp/utils/project_level_test.py @@ -0,0 +1,43 @@ +""" +Tests for mapping OWASP official numeric project levels to internal ProjectLevel enums. + +These tests ensure that the utility correctly translates official OWASP project +classification values into the corresponding Nest ProjectLevel values and +handles invalid or unsupported inputs safely. +""" + +from decimal import Decimal + +from django.test import SimpleTestCase + +from apps.owasp.models.enums.project import ProjectLevel +from apps.owasp.utils.project_level import map_level + + +class ProjectLevelMappingTest(SimpleTestCase): + """Test cases for the `map_level` utility function.""" + + def test_flagship_levels(self): + """Flagship projects should be mapped from levels 4 and 3.5.""" + assert map_level(Decimal(4)) == ProjectLevel.FLAGSHIP + assert map_level(Decimal("3.5")) == ProjectLevel.FLAGSHIP + + def test_production_level(self): + """Production projects should be mapped from level 3.""" + assert map_level(Decimal(3)) == ProjectLevel.PRODUCTION + + def test_incubator_level(self): + """Incubator projects should be mapped from level 2.""" + assert map_level(Decimal(2)) == ProjectLevel.INCUBATOR + + def test_lab_level(self): + """Lab projects should be mapped from level 1.""" + assert map_level(Decimal(1)) == ProjectLevel.LAB + + def test_other_level(self): + """Other projects should be mapped from level 0.""" + assert map_level(Decimal(0)) == ProjectLevel.OTHER + + def test_negative_level_returns_none(self): + """Negative or invalid levels should not map to any ProjectLevel.""" + assert map_level(Decimal(-1)) is None From f00db447d36b31e827ecdfb1fbd20e9d8bcca1a1 Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Wed, 14 Jan 2026 18:13:19 +0000 Subject: [PATCH 05/10] Fix docstring formatting and satisfy ruff checks --- .../commands/owasp_update_project_health_scores.py | 9 +++------ .../commands/owasp_update_project_level_compliance.py | 11 ++++------- backend/apps/owasp/utils/project_level.py | 8 +++----- .../owasp_update_project_level_compliance_test.py | 8 ++------ backend/tests/apps/owasp/utils/project_level_test.py | 3 +-- 5 files changed, 13 insertions(+), 26 deletions(-) diff --git a/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py b/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py index dba01dc64e..d3588fe703 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_health_scores.py @@ -1,5 +1,4 @@ -""" -Update OWASP project health scores. +"""Update OWASP project health scores. This command calculates health scores for OWASP projects based on defined metrics and requirement thresholds. @@ -17,8 +16,7 @@ class Command(BaseCommand): - """ - Compute and update project health scores. + """Compute and update project health scores. Health scores are derived from project metrics and requirement definitions. Projects that fail level @@ -29,8 +27,7 @@ class Command(BaseCommand): help = "Update OWASP project health scores." def handle(self, *args, **options): - """ - Calculate and persist project health scores. + """Calculate and persist project health scores. This method iterates over project health metrics, evaluates each metric against defined requirements, diff --git a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py index c9d3b175b2..9c179b3521 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py @@ -1,5 +1,4 @@ -""" -Update OWASP project level compliance status. +"""Update OWASP project level compliance status. This command compares locally stored project levels against official OWASP project classification data and determines @@ -31,20 +30,18 @@ def normalize_name(name: str) -> str: class Command(BaseCommand): - """ - Detect and persist OWASP project level compliance. + """Detect and persist OWASP project level compliance. This command fetches official OWASP project level data, maps it to the internal ProjectLevel enum, and updates - project health metrics to indicate whether a project’s + project health metrics to indicate whether a project's stored level matches the official classification. """ help = "Detect and flag OWASP project level non-compliance." def handle(self, *args, **options) -> None: - """ - Execute project level compliance detection. + """Execute project level compliance detection. For each project health metric entry, this method determines the expected project level based on diff --git a/backend/apps/owasp/utils/project_level.py b/backend/apps/owasp/utils/project_level.py index abe5a1019a..c339a6fcb7 100644 --- a/backend/apps/owasp/utils/project_level.py +++ b/backend/apps/owasp/utils/project_level.py @@ -1,6 +1,4 @@ -""" -Utilities for mapping OWASP official numeric project levels -to internal ProjectLevel enum values. +"""Utilities for mapping OWASP official numeric project levels. This module acts as a translation layer between the OWASP project level definitions published upstream and the @@ -12,7 +10,6 @@ from apps.owasp.models.enums.project import ProjectLevel - _LEVEL_MAP = { Decimal(4): ProjectLevel.FLAGSHIP, Decimal("3.5"): ProjectLevel.FLAGSHIP, @@ -32,6 +29,7 @@ def map_level(level: Decimal) -> ProjectLevel | None: Returns: ProjectLevel | None: The mapped ProjectLevel value if valid, otherwise None for unsupported or invalid levels. + """ try: parsed_level = Decimal(str(level)) @@ -41,4 +39,4 @@ def map_level(level: Decimal) -> ProjectLevel | None: if parsed_level < 0: return None - return cast(ProjectLevel | None, _LEVEL_MAP.get(parsed_level)) + return cast("ProjectLevel | None", _LEVEL_MAP.get(parsed_level)) diff --git a/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py b/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py index 8b0ab140e7..f98aba4394 100644 --- a/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py +++ b/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py @@ -15,9 +15,7 @@ class TestProjectLevelCompliance(SimpleTestCase): "apps.owasp.management.commands.owasp_update_project_level_compliance." "ProjectHealthMetrics.objects" ) - @patch( - "apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get" - ) + @patch("apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get") def test_marks_project_as_non_compliant_when_levels_differ( self, mock_get, mock_objects, mock_bulk_save ): @@ -46,9 +44,7 @@ def test_marks_project_as_non_compliant_when_levels_differ( "apps.owasp.management.commands.owasp_update_project_level_compliance." "ProjectHealthMetrics.objects" ) - @patch( - "apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get" - ) + @patch("apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get") def test_project_remains_compliant_when_levels_match( self, mock_get, mock_objects, mock_bulk_save ): diff --git a/backend/tests/apps/owasp/utils/project_level_test.py b/backend/tests/apps/owasp/utils/project_level_test.py index d1a662b82c..e0db3c3c4f 100644 --- a/backend/tests/apps/owasp/utils/project_level_test.py +++ b/backend/tests/apps/owasp/utils/project_level_test.py @@ -1,5 +1,4 @@ -""" -Tests for mapping OWASP official numeric project levels to internal ProjectLevel enums. +"""Tests for mapping OWASP official numeric project levels to internal ProjectLevel enums. These tests ensure that the utility correctly translates official OWASP project classification values into the corresponding Nest ProjectLevel values and From 042ddd65c40a53bca5c6b4cf49a97bff352a4f64 Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Wed, 14 Jan 2026 18:21:22 +0000 Subject: [PATCH 06/10] Use latest health metrics for project level compliance check --- .../owasp_update_project_level_compliance.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py index 9c179b3521..e8f41d463e 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py @@ -48,9 +48,15 @@ def handle(self, *args, **options) -> None: official OWASP data and marks the project as level non-compliant when a mismatch is detected. """ - response = requests.get(LEVELS_URL, timeout=15) - response.raise_for_status() - official_data = response.json() + try: + response = requests.get(LEVELS_URL, timeout=15) + response.raise_for_status() + official_data = response.json() + except requests.RequestException as exc: + self.stdout.write( + self.style.ERROR(f"Failed to fetch official project levels: {exc}") + ) + return by_repo: dict[str, Decimal] = {} by_name: dict[str, Decimal] = {} @@ -68,7 +74,7 @@ def handle(self, *args, **options) -> None: if name := item.get("name"): by_name[normalize_name(name)] = level - metrics = ProjectHealthMetrics.objects.select_related("project") + metrics = ProjectHealthMetrics.get_latest_health_metrics().select_related("project") updated_metrics = [] for metric in metrics: From 69e3e23de0a904dddb32cbbcbe63021b2ff05163 Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Thu, 15 Jan 2026 00:43:08 +0000 Subject: [PATCH 07/10] Update project level compliance using latest metrics and align tests --- .../owasp_update_project_level_compliance.py | 14 +++++------- ...sp_update_project_level_compliance_test.py | 22 ++++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py index e8f41d463e..7e5482911c 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py @@ -49,14 +49,12 @@ def handle(self, *args, **options) -> None: level non-compliant when a mismatch is detected. """ try: - response = requests.get(LEVELS_URL, timeout=15) - response.raise_for_status() - official_data = response.json() - except requests.RequestException as exc: - self.stdout.write( - self.style.ERROR(f"Failed to fetch official project levels: {exc}") - ) - return + response = requests.get(LEVELS_URL, timeout=15) + response.raise_for_status() + official_data = response.json() + except requests.RequestException as exc: + self.stderr.write(self.style.ERROR(f"Failed to fetch official project levels: {exc}")) + return by_repo: dict[str, Decimal] = {} by_name: dict[str, Decimal] = {} diff --git a/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py b/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py index f98aba4394..66568fc3fe 100644 --- a/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py +++ b/backend/tests/apps/owasp/management/commands/owasp_update_project_level_compliance_test.py @@ -7,17 +7,17 @@ class TestProjectLevelCompliance(SimpleTestCase): + @patch("apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get") @patch( "apps.owasp.management.commands.owasp_update_project_level_compliance." - "ProjectHealthMetrics.bulk_save" + "ProjectHealthMetrics.get_latest_health_metrics" ) @patch( "apps.owasp.management.commands.owasp_update_project_level_compliance." - "ProjectHealthMetrics.objects" + "ProjectHealthMetrics.bulk_save" ) - @patch("apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get") def test_marks_project_as_non_compliant_when_levels_differ( - self, mock_get, mock_objects, mock_bulk_save + self, mock_bulk_save, mock_get_latest, mock_get ): mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = [ @@ -27,26 +27,27 @@ def test_marks_project_as_non_compliant_when_levels_differ( metric = MagicMock() metric.project.name = "Test Project" metric.project.level = ProjectLevel.PRODUCTION + metric.project.owasp_url = None metric.level_non_compliant = False - mock_objects.select_related.return_value = [metric] + mock_get_latest.return_value.select_related.return_value = [metric] call_command("owasp_update_project_level_compliance") assert metric.level_non_compliant is True mock_bulk_save.assert_called_once() + @patch("apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get") @patch( "apps.owasp.management.commands.owasp_update_project_level_compliance." - "ProjectHealthMetrics.bulk_save" + "ProjectHealthMetrics.get_latest_health_metrics" ) @patch( "apps.owasp.management.commands.owasp_update_project_level_compliance." - "ProjectHealthMetrics.objects" + "ProjectHealthMetrics.bulk_save" ) - @patch("apps.owasp.management.commands.owasp_update_project_level_compliance.requests.get") def test_project_remains_compliant_when_levels_match( - self, mock_get, mock_objects, mock_bulk_save + self, mock_bulk_save, mock_get_latest, mock_get ): mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = [ @@ -56,9 +57,10 @@ def test_project_remains_compliant_when_levels_match( metric = MagicMock() metric.project.name = "Test Project" metric.project.level = ProjectLevel.PRODUCTION + metric.project.owasp_url = None metric.level_non_compliant = False - mock_objects.select_related.return_value = [metric] + mock_get_latest.return_value.select_related.return_value = [metric] call_command("owasp_update_project_level_compliance") From 9cc930dfa7d234e5cd2946eb40eb556a8a257ff3 Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Thu, 15 Jan 2026 00:58:45 +0000 Subject: [PATCH 08/10] Handle JSON decode errors when fetching project levels --- .../commands/owasp_update_project_level_compliance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py index 7e5482911c..a8c36e9a30 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py @@ -52,7 +52,7 @@ def handle(self, *args, **options) -> None: response = requests.get(LEVELS_URL, timeout=15) response.raise_for_status() official_data = response.json() - except requests.RequestException as exc: + except (requests.RequestException, ValueError) as exc: self.stderr.write(self.style.ERROR(f"Failed to fetch official project levels: {exc}")) return From d6bca1ad944c2d972c6d34ba82fa8144ba670385 Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Wed, 4 Feb 2026 20:37:06 +0000 Subject: [PATCH 09/10] Fix incorrect compliance update count after bulk save --- .../commands/owasp_update_project_level_compliance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py index a8c36e9a30..b42a17675c 100644 --- a/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py +++ b/backend/apps/owasp/management/commands/owasp_update_project_level_compliance.py @@ -96,6 +96,8 @@ def handle(self, *args, **options) -> None: metric.level_non_compliant = project.level != expected_level updated_metrics.append(metric) + updated_count = len(updated_metrics) + if updated_metrics: ProjectHealthMetrics.bulk_save( updated_metrics, @@ -103,5 +105,5 @@ def handle(self, *args, **options) -> None: ) self.stdout.write( - self.style.SUCCESS(f"Updated level compliance for {len(updated_metrics)} projects.") + self.style.SUCCESS(f"Updated level compliance for {updated_count} projects.") ) From 1ca60a16aacb18b10ca9ee22b83d506bb16cecb0 Mon Sep 17 00:00:00 2001 From: saichethana28 Date: Wed, 4 Feb 2026 20:57:18 +0000 Subject: [PATCH 10/10] Fix NaN issue --- backend/apps/owasp/utils/project_level.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/apps/owasp/utils/project_level.py b/backend/apps/owasp/utils/project_level.py index c339a6fcb7..7341d57bf5 100644 --- a/backend/apps/owasp/utils/project_level.py +++ b/backend/apps/owasp/utils/project_level.py @@ -36,6 +36,9 @@ def map_level(level: Decimal) -> ProjectLevel | None: except (InvalidOperation, TypeError, ValueError): return None + if not parsed_level.is_finite(): + return None + if parsed_level < 0: return None