feat(prospecting): implement security audit pipeline (Workstream 2A)
Complete security audit integration into the enrichment pipeline:
Backend:
- SecurityAuditService with 7 passive checks: HTTPS, SSL cert, security
headers, exposed files, cookies, server info, technology detection
- Constants file with SECURITY_HEADERS, EXPOSED_PATHS, SEVERITY_SCORES
- SecurityAuditResponse schema with JSON field validators + aliases
- Endpoints: POST /security-audit/{id}, POST /security-audit/batch
- Added to full_enrichment pipeline (Step 5, before scoring)
- get_pending_security_audit() query in prospect_service
Frontend:
- Security tab on prospect detail page with grade badge (A+ to F),
score/100, severity counts, HTTPS/SSL status, missing headers,
exposed files, technologies, and full findings list
- "Run Security Audit" button with loading state
- "Security Audit" batch button on scan-jobs page
Tested on batirenovation-strasbourg.fr: Grade D (50/100), 11 issues
found (missing headers, exposed wp-login, server version disclosure).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
82
app/modules/prospecting/schemas/security_audit.py
Normal file
82
app/modules/prospecting/schemas/security_audit.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# app/modules/prospecting/schemas/security_audit.py
|
||||
"""Pydantic schemas for security audit responses."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class SecurityAuditFinding(BaseModel):
|
||||
"""A single security finding."""
|
||||
|
||||
title: str
|
||||
severity: str
|
||||
category: str
|
||||
detail: str
|
||||
is_positive: bool = False
|
||||
|
||||
|
||||
class SecurityAuditResponse(BaseModel):
|
||||
"""Schema for security audit detail response."""
|
||||
|
||||
id: int
|
||||
prospect_id: int
|
||||
score: int
|
||||
grade: str
|
||||
detected_language: str | None = None
|
||||
findings: list[SecurityAuditFinding] = Field(default=[], validation_alias="findings_json")
|
||||
findings_count_critical: int = 0
|
||||
findings_count_high: int = 0
|
||||
findings_count_medium: int = 0
|
||||
findings_count_low: int = 0
|
||||
findings_count_info: int = 0
|
||||
has_https: bool | None = None
|
||||
has_valid_ssl: bool | None = None
|
||||
ssl_expires_at: datetime | None = None
|
||||
missing_headers: list[str] = Field(default=[], validation_alias="missing_headers_json")
|
||||
exposed_files: list[str] = Field(default=[], validation_alias="exposed_files_json")
|
||||
technologies: list[str] = Field(default=[], validation_alias="technologies_json")
|
||||
scan_error: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@field_validator("findings", mode="before")
|
||||
@classmethod
|
||||
def parse_findings(cls, v):
|
||||
if isinstance(v, str):
|
||||
return json.loads(v)
|
||||
return v
|
||||
|
||||
@field_validator("missing_headers", mode="before")
|
||||
@classmethod
|
||||
def parse_missing_headers(cls, v):
|
||||
if isinstance(v, str):
|
||||
return json.loads(v)
|
||||
return v or []
|
||||
|
||||
@field_validator("exposed_files", mode="before")
|
||||
@classmethod
|
||||
def parse_exposed_files(cls, v):
|
||||
if isinstance(v, str):
|
||||
return json.loads(v)
|
||||
return v or []
|
||||
|
||||
@field_validator("technologies", mode="before")
|
||||
@classmethod
|
||||
def parse_technologies(cls, v):
|
||||
if isinstance(v, str):
|
||||
return json.loads(v)
|
||||
return v or []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SecurityAuditSingleResponse(BaseModel):
|
||||
"""Response for single-prospect security audit."""
|
||||
|
||||
domain: str
|
||||
score: int
|
||||
grade: str
|
||||
findings_count: int
|
||||
Reference in New Issue
Block a user