diff --git a/app/modules/prospecting/models/__init__.py b/app/modules/prospecting/models/__init__.py index f85c4f84..e231aaae 100644 --- a/app/modules/prospecting/models/__init__.py +++ b/app/modules/prospecting/models/__init__.py @@ -22,6 +22,7 @@ from app.modules.prospecting.models.prospect import ( from app.modules.prospecting.models.prospect_contact import ContactType, ProspectContact from app.modules.prospecting.models.prospect_score import ProspectScore from app.modules.prospecting.models.scan_job import JobStatus, JobType, ProspectScanJob +from app.modules.prospecting.models.security_audit import ProspectSecurityAudit from app.modules.prospecting.models.tech_profile import ProspectTechProfile __all__ = [ @@ -44,4 +45,5 @@ __all__ = [ "CampaignChannel", "CampaignSendStatus", "LeadType", + "ProspectSecurityAudit", ] diff --git a/app/modules/prospecting/models/prospect.py b/app/modules/prospecting/models/prospect.py index 3a3065c6..490f1852 100644 --- a/app/modules/prospecting/models/prospect.py +++ b/app/modules/prospecting/models/prospect.py @@ -69,10 +69,12 @@ class Prospect(Base, TimestampMixin): last_tech_scan_at = Column(DateTime, nullable=True) last_perf_scan_at = Column(DateTime, nullable=True) last_contact_scrape_at = Column(DateTime, nullable=True) + last_security_audit_at = Column(DateTime, nullable=True) # Relationships tech_profile = relationship("ProspectTechProfile", back_populates="prospect", uselist=False, cascade="all, delete-orphan") performance_profile = relationship("ProspectPerformanceProfile", back_populates="prospect", uselist=False, cascade="all, delete-orphan") + security_audit = relationship("ProspectSecurityAudit", back_populates="prospect", uselist=False, cascade="all, delete-orphan") score = relationship("ProspectScore", back_populates="prospect", uselist=False, cascade="all, delete-orphan") contacts = relationship("ProspectContact", back_populates="prospect", cascade="all, delete-orphan") interactions = relationship("ProspectInteraction", back_populates="prospect", cascade="all, delete-orphan") diff --git a/app/modules/prospecting/models/security_audit.py b/app/modules/prospecting/models/security_audit.py new file mode 100644 index 00000000..005e8dd1 --- /dev/null +++ b/app/modules/prospecting/models/security_audit.py @@ -0,0 +1,59 @@ +# app/modules/prospecting/models/security_audit.py +""" +Security audit results for a prospect's website. + +Stores findings from passive security checks (HTTPS, headers, exposed files, +cookies, server info, technology detection). Follows the same 1:1 pattern as +ProspectTechProfile and ProspectPerformanceProfile. +""" + +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy.orm import relationship + +from app.core.database import Base +from models.database.base import TimestampMixin + + +class ProspectSecurityAudit(Base, TimestampMixin): + """Security audit results for a prospect's website.""" + + __tablename__ = "prospect_security_audits" + + id = Column(Integer, primary_key=True, index=True) + prospect_id = Column( + Integer, + ForeignKey("prospects.id", ondelete="CASCADE"), + nullable=False, + unique=True, + ) + + # Overall score and grade + score = Column(Integer, nullable=False, default=0) # 0-100 + grade = Column(String(2), nullable=False, default="F") # A+, A, B, C, D, F + + # Detected language for bilingual reports + detected_language = Column(String(5), nullable=True, default="en") + + # Findings stored as JSON (variable structure per check) + findings_json = Column(Text, nullable=True) # JSON list of finding dicts + + # Denormalized severity counts (for dashboard queries without JSON parsing) + findings_count_critical = Column(Integer, nullable=False, default=0) + findings_count_high = Column(Integer, nullable=False, default=0) + findings_count_medium = Column(Integer, nullable=False, default=0) + findings_count_low = Column(Integer, nullable=False, default=0) + findings_count_info = Column(Integer, nullable=False, default=0) + + # Key results (denormalized for quick access) + has_https = Column(Boolean, nullable=True) + has_valid_ssl = Column(Boolean, nullable=True) + ssl_expires_at = Column(DateTime, nullable=True) + missing_headers_json = Column(Text, nullable=True) # JSON list of header names + exposed_files_json = Column(Text, nullable=True) # JSON list of exposed paths + technologies_json = Column(Text, nullable=True) # JSON list of detected techs + + # Scan metadata + scan_error = Column(Text, nullable=True) + + # Relationships + prospect = relationship("Prospect", back_populates="security_audit")