From 972ee1e5d035ed677b00c223a99a21bf9e4880e9 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Mon, 30 Mar 2026 22:23:38 +0200 Subject: [PATCH] feat(prospecting): add ProspectSecurityAudit model (Phase 1 foundation) - New model: ProspectSecurityAudit with score, grade, findings_json, severity counts, has_https, has_valid_ssl, missing_headers, exposed files, technologies, scan_error - Add last_security_audit_at timestamp to Prospect model - Add security_audit 1:1 relationship on Prospect Part of Phase 1: Security Audit in Enrichment Pipeline. Service, constants, migration, endpoints, and frontend to follow. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/modules/prospecting/models/__init__.py | 2 + app/modules/prospecting/models/prospect.py | 2 + .../prospecting/models/security_audit.py | 59 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 app/modules/prospecting/models/security_audit.py 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")