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) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 22:23:38 +02:00
parent 70f2803dd3
commit 972ee1e5d0
3 changed files with 63 additions and 0 deletions

View File

@@ -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",
]

View File

@@ -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")

View File

@@ -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")