Cybersecurity-Projects/PROJECTS/bug-bounty-platform/backend/app/report/Report.py

186 lines
4.6 KiB
Python

"""
ⒸAngelaMos | 2025
Report.py
"""
from __future__ import annotations
from datetime import UTC, datetime
from decimal import Decimal
from typing import TYPE_CHECKING
from uuid import UUID
from sqlalchemy import DateTime, ForeignKey, Numeric, String, Text
from sqlalchemy.orm import (
Mapped,
mapped_column,
relationship,
)
from config import (
CWE_ID_MAX_LENGTH,
REPORT_TITLE_MAX_LENGTH,
ReportStatus,
SafeEnum,
Severity,
)
from core.Base import (
Base,
TimestampMixin,
UUIDMixin,
)
if TYPE_CHECKING:
from program.Program import Program
from report.Attachment import Attachment
from report.Comment import Comment
from user.User import User
class Report(Base, UUIDMixin, TimestampMixin):
"""
Vulnerability report submitted by a researcher
"""
__tablename__ = "reports"
program_id: Mapped[UUID] = mapped_column(
ForeignKey("programs.id",
ondelete = "CASCADE"),
index = True,
)
researcher_id: Mapped[UUID] = mapped_column(
ForeignKey("users.id",
ondelete = "CASCADE"),
index = True,
)
title: Mapped[str] = mapped_column(
String(REPORT_TITLE_MAX_LENGTH),
)
description: Mapped[str] = mapped_column(Text)
steps_to_reproduce: Mapped[str | None] = mapped_column(
Text,
default = None,
)
impact: Mapped[str | None] = mapped_column(
Text,
default = None,
)
severity_submitted: Mapped[Severity] = mapped_column(
SafeEnum(Severity),
default = Severity.MEDIUM,
)
severity_final: Mapped[Severity | None] = mapped_column(
SafeEnum(Severity),
default = None,
)
status: Mapped[ReportStatus] = mapped_column(
SafeEnum(ReportStatus),
default = ReportStatus.NEW,
index = True,
)
cvss_score: Mapped[Decimal | None] = mapped_column(
Numeric(precision = 3,
scale = 1),
default = None,
)
cwe_id: Mapped[str | None] = mapped_column(
String(CWE_ID_MAX_LENGTH),
default = None,
)
bounty_amount: Mapped[int | None] = mapped_column(default = None)
duplicate_of_id: Mapped[UUID | None] = mapped_column(
ForeignKey("reports.id",
ondelete = "SET NULL"),
default = None,
)
triaged_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone = True),
default = None,
)
resolved_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone = True),
default = None,
)
disclosed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone = True),
default = None,
)
program: Mapped[Program] = relationship(
back_populates = "reports",
lazy = "raise",
)
researcher: Mapped[User] = relationship(
back_populates = "reports",
lazy = "raise",
)
comments: Mapped[list[Comment]] = relationship(
back_populates = "report",
cascade = "all, delete-orphan",
lazy = "raise",
)
attachments: Mapped[list[Attachment]] = relationship(
back_populates = "report",
cascade = "all, delete-orphan",
lazy = "raise",
)
duplicate_of: Mapped[Report | None] = relationship(
remote_side = "Report.id",
lazy = "raise",
)
def mark_triaging(self) -> None:
"""
Transition report to triaging status
"""
self.status = ReportStatus.TRIAGING
self.triaged_at = datetime.now(UTC)
def mark_resolved(self) -> None:
"""
Transition report to resolved status
"""
self.status = ReportStatus.RESOLVED
self.resolved_at = datetime.now(UTC)
def mark_disclosed(self) -> None:
"""
Transition report to disclosed status
"""
self.status = ReportStatus.DISCLOSED
self.disclosed_at = datetime.now(UTC)
@property
def is_open(self) -> bool:
"""
Check if report is still open for action
"""
return self.status in (
ReportStatus.NEW,
ReportStatus.TRIAGING,
ReportStatus.NEEDS_MORE_INFO,
)
@property
def is_closed(self) -> bool:
"""
Check if report has reached a terminal state
"""
return self.status in (
ReportStatus.ACCEPTED,
ReportStatus.DUPLICATE,
ReportStatus.INFORMATIVE,
ReportStatus.NOT_APPLICABLE,
ReportStatus.RESOLVED,
ReportStatus.DISCLOSED,
)