244 lines
7.2 KiB
Python
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()
|