Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
949afda
WIP: Implementation of management command.
SUPERGAMERDIV Aug 14, 2025
e28ebc5
fix project_levels_url
divyanshu-vr Aug 14, 2025
038c71d
avoid string literal in raise
divyanshu-vr Aug 14, 2025
ff4df3b
wrap db updates in transaction
divyanshu-vr Aug 14, 2025
d2fea99
use logger.exception
divyanshu-vr Aug 14, 2025
b652e64
Apply suggestions from code review
divyanshu-vr Aug 14, 2025
50127c3
Apply suggestions from code review
divyanshu-vr Aug 14, 2025
4e11688
Apply suggestions from code review
divyanshu-vr Aug 14, 2025
75dffda
coderabbit suggestions fixed
SUPERGAMERDIV Aug 14, 2025
df2f113
Merge branch 'feature/ProjectLevelComplianceDetection' of https://git…
SUPERGAMERDIV Aug 14, 2025
d85e0d5
conflict fixes
SUPERGAMERDIV Aug 14, 2025
d3bd32b
pre-commit fixes
SUPERGAMERDIV Aug 14, 2025
1365beb
fixes and added CronJob Scheduling
SUPERGAMERDIV Aug 18, 2025
41598c9
coderabbit fixes
SUPERGAMERDIV Aug 18, 2025
9199bd2
Merge branch 'OWASP:main' into feature/ProjectLevelComplianceDetection
divyanshu-vr Aug 21, 2025
64ee6bb
made changes asked.
SUPERGAMERDIV Aug 21, 2025
e1c923b
Merge branch 'feature/ProjectLevelComplianceDetection' of https://git…
SUPERGAMERDIV Aug 21, 2025
8f4e3a4
Update backend/apps/owasp/management/commands/owasp_update_project_he…
divyanshu-vr Aug 21, 2025
021ec86
fixed sonarqube issues
SUPERGAMERDIV Aug 23, 2025
b7a7eac
Update __init__.py
divyanshu-vr Aug 24, 2025
79acaec
Update __init__.py
divyanshu-vr Aug 24, 2025
2748680
sonar fixes
SUPERGAMERDIV Aug 26, 2025
d5aed68
Merge branch 'feature/ProjectLevelComplianceDetection' of https://git…
SUPERGAMERDIV Aug 26, 2025
b886714
fixed make check errors
SUPERGAMERDIV Aug 28, 2025
55bcf9d
fixed sonarqube and precommit errors
SUPERGAMERDIV Aug 28, 2025
1e20a06
made coderabbit suggestions
SUPERGAMERDIV Aug 28, 2025
9b3205d
test fixes
SUPERGAMERDIV Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ shell-db:

sync-data: \
update-data \
owasp-update-project-health-metrics \
owasp-update-project-health-scores \
Comment on lines +111 to +112
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The health metric generation is a separate step, we should not mix them.

enrich-data \
index-data

Expand All @@ -133,6 +135,7 @@ update-data: \
github-update-related-organizations \
github-update-users \
owasp-aggregate-projects \
owasp-sync-official-project-levels \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic behind this target should be a part of github-update-owasp-organization

owasp-update-events \
owasp-sync-posts \
owasp-update-sponsors \
Expand Down
4 changes: 4 additions & 0 deletions backend/apps/owasp/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ owasp-update-project-health-metrics:
@echo "Updating OWASP project health metrics"
@CMD="python manage.py owasp_update_project_health_metrics" $(MAKE) exec-backend-command

owasp-sync-official-project-levels:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not right coupling decision. I suggested to implement the official project levels during the project data sync. The health metrics is the next step after data sync is done.

@echo "Syncing official OWASP project levels"
@CMD="python manage.py owasp_update_project_health_metrics --sync-official-levels-only" $(MAKE) exec-backend-command

owasp-update-project-health-requirements:
@echo "Updating OWASP project health requirements"
@CMD="python manage.py owasp_update_project_health_requirements" $(MAKE) exec-backend-command
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""A command to detect and report project level compliance status."""

import logging

from django.core.management.base import BaseCommand

from apps.owasp.models.project import Project

logger = logging.getLogger(__name__)


class Command(BaseCommand):
"""Command to detect and report project level compliance status.

This is a reporting command only - it does not sync or update any data.
For data synchronization, use the main data pipeline: make sync-data

Architecture:
- Part 1: Official level syncing happens during 'make update-data'
- Part 2: Health scoring with compliance penalties happens during 'make sync-data'
- This command: Reporting and analysis only
"""

help = "Detect and report projects with non-compliant level assignments"

def add_arguments(self, parser):
"""Add command line arguments."""
parser.add_argument(
"--verbose",
action="store_true",
help="Enable verbose output showing all projects",
)

def handle(self, *args, **options):
"""Execute compliance detection and reporting."""
verbose = options["verbose"]

self.stdout.write("Analyzing project level compliance status...")

# Get all active projects
active_projects = Project.objects.filter(is_active=True).select_related()

compliant_projects = []
non_compliant_projects = []

for project in active_projects:
if project.is_level_compliant:
compliant_projects.append(project)
if verbose:
self.stdout.write(f"✓ {project.name}: {project.level} (matches official)")
else:
non_compliant_projects.append(project)
self.stdout.write(
self.style.WARNING(
f"✗ {project.name}: Local={project.level}, "
f"Official={project.project_level_official}"
)
)

# Summary statistics
total_projects = len(active_projects)
compliant_count = len(compliant_projects)
non_compliant_count = len(non_compliant_projects)
compliance_rate = (compliant_count / total_projects * 100) if total_projects else 0.0

self.stdout.write("\n" + "=" * 60)
self.stdout.write("PROJECT LEVEL COMPLIANCE SUMMARY")
self.stdout.write("=" * 60)
self.stdout.write(f"Total active projects: {total_projects}")
self.stdout.write(f"Compliant projects: {compliant_count}")
self.stdout.write(f"Non-compliant projects: {non_compliant_count}")
self.stdout.write(f"Compliance rate: {compliance_rate:.1f}%")

if non_compliant_count > 0:
warning_msg = f"WARNING: Found {non_compliant_count} non-compliant projects"
self.stdout.write(f"\n{self.style.WARNING(warning_msg)}")
penalty_msg = (
"These projects will receive score penalties in the next health score update."
)
self.stdout.write(penalty_msg)
else:
self.stdout.write(f"\n{self.style.SUCCESS('✓ All projects are level compliant!')}")

# Log summary for monitoring
logger.info(
"Project level compliance analysis completed",
extra={
"total_projects": total_projects,
"compliant_projects": compliant_count,
"non_compliant_projects": non_compliant_count,
"compliance_rate": f"{compliance_rate:.1f}%",
},
)

# Check if official levels are populated
from apps.owasp.models.enums.project import ProjectLevel

default_level = ProjectLevel.OTHER
projects_without_official_level = sum(
1 for project in active_projects if project.project_level_official == default_level
)

if projects_without_official_level > 0:
info_msg = (
f"INFO: {projects_without_official_level} projects have default official levels"
)
self.stdout.write(f"\n{self.style.NOTICE(info_msg)}")
sync_msg = (
"Run 'make update-data' to sync official levels, "
"then 'make sync-data' for scoring."
)
self.stdout.write(sync_msg)
Original file line number Diff line number Diff line change
@@ -1,15 +1,164 @@
"""A command to update OWASP project health metrics."""

import logging

import requests
from django.core.management.base import BaseCommand
from requests.exceptions import RequestException

from apps.owasp.models.project import Project
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics

logger = logging.getLogger(__name__)

OWASP_PROJECT_LEVELS_URL = (
"https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/project_levels.json"
)


class Command(BaseCommand):
help = "Update OWASP project health metrics."

def add_arguments(self, parser):
"""Add command line arguments."""
parser.add_argument(
"--skip-official-levels",
action="store_true",
help="Skip fetching official project levels from OWASP GitHub repository",
)
parser.add_argument(
"--sync-official-levels-only",
action="store_true",
help="Only sync official project levels, skip health metrics updates",
)
parser.add_argument(
"--timeout",
type=int,
default=30,
help="HTTP timeout for fetching project levels (default: 30 seconds)",
)

def fetch_official_project_levels(self, timeout: int = 30) -> dict[str, str] | None:
"""Fetch project levels from OWASP GitHub repository.

Args:
timeout: HTTP request timeout in seconds

Returns:
Dict mapping project names to their official levels, or None if fetch fails

"""
try:
response = requests.get(
OWASP_PROJECT_LEVELS_URL,
timeout=timeout,
headers={"Accept": "application/json"},
)
response.raise_for_status()
data = response.json()
if not isinstance(data, list):
logger.exception(
"Invalid project levels data format",
extra={"expected": "list", "got": type(data).__name__},
)
return None

# Convert the list to a dict mapping project names to their levels
project_levels = {}
for entry in data:
if not isinstance(entry, dict):
continue
project_name = entry.get("name")
level = entry.get("level")
if (
isinstance(project_name, str)
and isinstance(level, (str, int, float))
and project_name.strip()
):
project_levels[project_name.strip()] = str(level)

except (RequestException, ValueError) as e:
logger.exception(
"Failed to fetch project levels",
extra={"url": OWASP_PROJECT_LEVELS_URL, "error": str(e)},
)
return None
else:
return project_levels

def update_official_levels(self, official_levels: dict[str, str]) -> int:
"""Update official levels for projects.

Args:
official_levels: Dict mapping project names to their official levels

Returns:
Number of projects updated

"""
updated_count = 0
projects_to_update = []

# Normalize official levels by stripping whitespace and normalizing case
normalized_official_levels = {
k.strip().lower(): v.strip().lower() for k, v in official_levels.items()
}

for project in Project.objects.filter(is_active=True):
normalized_project_name = project.name.strip().lower()
if normalized_project_name in normalized_official_levels:
official_level = normalized_official_levels[normalized_project_name]
# Map string levels to enum values
level_mapping = {
"incubator": "incubator",
"lab": "lab",
"production": "production",
"flagship": "flagship",
"2": "incubator",
"3": "lab",
"3.5": "production",
"4": "flagship",
}
mapped_level = level_mapping.get(official_level, "other")

if project.project_level_official != mapped_level:
project.project_level_official = mapped_level
projects_to_update.append(project)
updated_count += 1

if projects_to_update:
Project.bulk_save(projects_to_update, fields=["project_level_official"])
self.stdout.write(f"Updated official levels for {updated_count} projects")
else:
self.stdout.write("No official level updates needed")

return updated_count

def handle(self, *args, **options):
skip_official_levels = options["skip_official_levels"]
sync_official_levels_only = options["sync_official_levels_only"]
timeout = options["timeout"]

# Part 1: Sync official project levels during project sync
if not skip_official_levels:
self.stdout.write("Fetching official project levels from OWASP GitHub repository...")
official_levels = self.fetch_official_project_levels(timeout=timeout)
if official_levels:
success_msg = (
f"Successfully fetched {len(official_levels)} official project levels"
)
self.stdout.write(success_msg)
self.update_official_levels(official_levels)
else:
warning_msg = "Failed to fetch official project levels, continuing without updates"
self.stdout.write(self.style.WARNING(warning_msg))

# If only syncing official levels, stop here (Part 1 only)
if sync_official_levels_only:
self.stdout.write(self.style.SUCCESS("Official level sync completed."))
return

# Part 2: Update project health metrics (only if not sync-only mode)
metric_project_field_mapping = {
"contributors_count": "contributors_count",
"created_at": "created_at",
Expand Down
Loading