secure-web/backend/api/serializers.py

244 lines
7.2 KiB
Python

"""
DRF Serializers for the API.
This module defines serializers for converting model instances
to JSON and validating input data.
"""
from rest_framework import serializers
from websites.models import Website, Scan, Issue, Metric, ScanStatus
class IssueSerializer(serializers.ModelSerializer):
"""Serializer for Issue model."""
severity_display = serializers.CharField(source='get_severity_display', read_only=True)
category_display = serializers.CharField(source='get_category_display', read_only=True)
tool_display = serializers.CharField(source='get_tool_display', read_only=True)
class Meta:
model = Issue
fields = [
'id',
'category',
'category_display',
'severity',
'severity_display',
'tool',
'tool_display',
'title',
'description',
'affected_url',
'remediation',
'created_at',
]
read_only_fields = fields
class MetricSerializer(serializers.ModelSerializer):
"""Serializer for Metric model."""
formatted_value = serializers.CharField(source='get_formatted_value', read_only=True)
unit_display = serializers.CharField(source='get_unit_display', read_only=True)
class Meta:
model = Metric
fields = [
'id',
'name',
'display_name',
'value',
'unit',
'unit_display',
'formatted_value',
'source',
'score',
]
read_only_fields = fields
class ScanListSerializer(serializers.ModelSerializer):
"""Serializer for Scan list views (minimal data)."""
status_display = serializers.CharField(source='get_status_display', read_only=True)
website_url = serializers.CharField(source='website.url', read_only=True)
issues_count = serializers.SerializerMethodField()
class Meta:
model = Scan
fields = [
'id',
'website_url',
'status',
'status_display',
'created_at',
'completed_at',
'overall_score',
'performance_score',
'security_score',
'issues_count',
]
read_only_fields = fields
def get_issues_count(self, obj):
return obj.issues.count()
class ScanDetailSerializer(serializers.ModelSerializer):
"""Serializer for Scan detail views (full data)."""
status_display = serializers.CharField(source='get_status_display', read_only=True)
website_url = serializers.CharField(source='website.url', read_only=True)
website_domain = serializers.CharField(source='website.domain', read_only=True)
issues = IssueSerializer(many=True, read_only=True)
metrics = MetricSerializer(many=True, read_only=True)
issues_by_category = serializers.SerializerMethodField()
issues_by_severity = serializers.SerializerMethodField()
class Meta:
model = Scan
fields = [
'id',
'website_url',
'website_domain',
'status',
'status_display',
'created_at',
'started_at',
'completed_at',
'overall_score',
'performance_score',
'accessibility_score',
'seo_score',
'best_practices_score',
'security_score',
'error_message',
'issues',
'metrics',
'issues_by_category',
'issues_by_severity',
]
read_only_fields = fields
def get_issues_by_category(self, obj):
"""Group issues by category."""
from collections import defaultdict
grouped = defaultdict(list)
for issue in obj.issues.all():
grouped[issue.category].append(IssueSerializer(issue).data)
return dict(grouped)
def get_issues_by_severity(self, obj):
"""Count issues by severity."""
from django.db.models import Count
counts = obj.issues.values('severity').annotate(count=Count('id'))
return {item['severity']: item['count'] for item in counts}
class ScanCreateSerializer(serializers.Serializer):
"""Serializer for creating new scans."""
url = serializers.URLField(
required=True,
help_text="The URL to scan (must be http or https)"
)
def validate_url(self, value):
"""Validate and normalize the URL."""
from scanner.utils import validate_url
is_valid, result = validate_url(value)
if not is_valid:
raise serializers.ValidationError(result)
return result # Return normalized URL
def create(self, validated_data):
"""Create Website and Scan records."""
from scanner.tasks import check_rate_limit, check_concurrent_scan_limit, run_scan_task
url = validated_data['url']
# Check rate limit
rate_limit_error = check_rate_limit(url)
if rate_limit_error:
raise serializers.ValidationError({'url': rate_limit_error})
# Check concurrent scan limit
concurrent_error = check_concurrent_scan_limit()
if concurrent_error:
raise serializers.ValidationError({'non_field_errors': concurrent_error})
# Get or create Website
website, created = Website.objects.get_or_create(
url=url,
defaults={'domain': validated_data.get('domain', '')}
)
# Create Scan
scan = Scan.objects.create(
website=website,
status=ScanStatus.PENDING
)
# Trigger Celery task
task = run_scan_task.delay(str(scan.id))
# Update scan with task ID
scan.celery_task_id = task.id
scan.save(update_fields=['celery_task_id'])
return scan
class WebsiteSerializer(serializers.ModelSerializer):
"""Serializer for Website model."""
scans_count = serializers.SerializerMethodField()
latest_scan = serializers.SerializerMethodField()
class Meta:
model = Website
fields = [
'id',
'url',
'domain',
'created_at',
'last_scanned_at',
'scans_count',
'latest_scan',
]
read_only_fields = fields
def get_scans_count(self, obj):
return obj.scans.count()
def get_latest_scan(self, obj):
latest = obj.scans.first()
if latest:
return ScanListSerializer(latest).data
return None
class WebsiteDetailSerializer(WebsiteSerializer):
"""Detailed Website serializer with scan list."""
scans = ScanListSerializer(many=True, read_only=True)
class Meta(WebsiteSerializer.Meta):
fields = WebsiteSerializer.Meta.fields + ['scans']
class HealthCheckSerializer(serializers.Serializer):
"""Serializer for health check response."""
status = serializers.CharField()
database = serializers.CharField()
redis = serializers.CharField()
celery = serializers.CharField()
timestamp = serializers.DateTimeField()