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:
@@ -25,9 +25,15 @@ from app.modules.prospecting.schemas.enrichment import (
|
||||
ScanSingleResponse,
|
||||
ScoreComputeBatchResponse,
|
||||
)
|
||||
from app.modules.prospecting.schemas.security_audit import (
|
||||
SecurityAuditSingleResponse,
|
||||
)
|
||||
from app.modules.prospecting.services.enrichment_service import enrichment_service
|
||||
from app.modules.prospecting.services.prospect_service import prospect_service
|
||||
from app.modules.prospecting.services.scoring_service import scoring_service
|
||||
from app.modules.prospecting.services.security_audit_service import (
|
||||
security_audit_service,
|
||||
)
|
||||
from app.modules.prospecting.services.stats_service import stats_service
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
|
||||
@@ -113,6 +119,25 @@ def contact_scrape_batch(
|
||||
return ScanBatchResponse(processed=len(prospects), successful=count)
|
||||
|
||||
|
||||
@router.post("/security-audit/batch", response_model=ScanBatchResponse)
|
||||
def security_audit_batch(
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Run security audit for pending prospects."""
|
||||
job = stats_service.create_job(db, JobType.SECURITY_AUDIT)
|
||||
prospects = prospect_service.get_pending_security_audit(db, limit=limit)
|
||||
count = 0
|
||||
for prospect in prospects:
|
||||
result = security_audit_service.run_audit(db, prospect)
|
||||
if result:
|
||||
count += 1
|
||||
stats_service.complete_job(job, processed=len(prospects))
|
||||
db.commit()
|
||||
return ScanBatchResponse(processed=len(prospects), successful=count)
|
||||
|
||||
|
||||
@router.post("/score-compute/batch", response_model=ScoreComputeBatchResponse)
|
||||
def compute_scores_batch(
|
||||
limit: int = Query(500, ge=1, le=5000),
|
||||
@@ -182,6 +207,27 @@ def scrape_contacts_single(
|
||||
return ContactScrapeResponse(domain=prospect.domain_name, contacts_found=len(contacts))
|
||||
|
||||
|
||||
@router.post("/security-audit/{prospect_id}", response_model=SecurityAuditSingleResponse)
|
||||
def security_audit_single(
|
||||
prospect_id: int = Path(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Run security audit for a single prospect."""
|
||||
prospect = prospect_service.get_by_id(db, prospect_id)
|
||||
audit = security_audit_service.run_audit(db, prospect)
|
||||
db.commit()
|
||||
findings_count = 0
|
||||
if audit:
|
||||
findings_count = audit.findings_count_critical + audit.findings_count_high + audit.findings_count_medium + audit.findings_count_low
|
||||
return SecurityAuditSingleResponse(
|
||||
domain=prospect.domain_name,
|
||||
score=audit.score if audit else 0,
|
||||
grade=audit.grade if audit else "F",
|
||||
findings_count=findings_count,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/full/{prospect_id}", response_model=FullEnrichmentResponse)
|
||||
def full_enrichment(
|
||||
prospect_id: int = Path(...),
|
||||
@@ -209,7 +255,11 @@ def full_enrichment(
|
||||
if prospect.has_website:
|
||||
contacts = enrichment_service.scrape_contacts(db, prospect)
|
||||
|
||||
# Step 5: Compute score
|
||||
# Step 5: Security audit (if has website)
|
||||
if prospect.has_website:
|
||||
security_audit_service.run_audit(db, prospect)
|
||||
|
||||
# Step 6: Compute score
|
||||
db.refresh(prospect)
|
||||
score = scoring_service.compute_score(db, prospect)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user