espace-paie-odentas/app/api/cron/send-csp-report/route.ts
odentas 65d367cb5f fix: Supprimer endpoints pdf-proxy et pdf-clean avec CORS ouvert
- Suppression de /app/api/pdf-proxy/route.ts (endpoint inutilisé avec CORS *)
- Suppression de /app/api/pdf-clean/route.ts (endpoint inutilisé avec CORS *)
- Mise à jour du rapport d'audit de sécurité
- Les PDFs sont désormais affichés via URLs présignées S3 directes
2025-11-14 19:56:37 +01:00

242 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/api/cron/send-csp-report/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createSbServiceRole } from '@/lib/supabaseServer';
import { sendUniversalEmailV2 } from '@/lib/emailTemplateService';
export const dynamic = 'force-dynamic';
// Protection : Vérifier le token Vercel Cron
function isValidCronRequest(req: NextRequest): boolean {
const authHeader = req.headers.get('authorization');
const cronSecret = process.env.CRON_SECRET;
// En développement, accepter sans secret
if (process.env.NODE_ENV === 'development') {
return true;
}
// En production, vérifier le secret
if (!cronSecret) {
console.error('❌ CRON_SECRET non configuré');
return false;
}
return authHeader === `Bearer ${cronSecret}`;
}
interface ViolationSummary {
violated_directive: string;
violation_count: number;
unique_blocked_uris: number;
affected_pages: number;
examples: Array<{
blocked_uri: string;
document_uri: string;
count: number;
}>;
}
export async function GET(req: NextRequest) {
try {
// Vérification sécurité
if (!isValidCronRequest(req)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const supabase = createSbServiceRole();
const today = new Date().toISOString().split('T')[0];
// Vérifier si un email a déjà été envoyé aujourd'hui
const { data: existingLog } = await supabase
.from('csp_email_logs')
.select('*')
.eq('report_date', today)
.maybeSingle();
if (existingLog) {
console.log(' Email CSP déjà envoyé aujourd\'hui');
return NextResponse.json({
message: 'Email already sent today',
sent_at: existingLog.email_sent_at
});
}
// Récupérer les violations des dernières 24h
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const { data: reports, error: reportsError } = await supabase
.from('csp_reports')
.select('*')
.gte('created_at', twentyFourHoursAgo)
.order('created_at', { ascending: false });
if (reportsError) {
throw reportsError;
}
// Si aucune violation, ne pas envoyer d'email
if (!reports || reports.length === 0) {
console.log('✅ Aucune violation CSP dans les dernières 24h');
return NextResponse.json({
message: 'No violations to report',
period: 'last 24 hours'
});
}
// Agréger les violations par directive
const violationsByDirective = new Map<string, ViolationSummary>();
reports.forEach(report => {
const directive = report.violated_directive;
if (!violationsByDirective.has(directive)) {
violationsByDirective.set(directive, {
violated_directive: directive,
violation_count: 0,
unique_blocked_uris: 0,
affected_pages: 0,
examples: []
});
}
const summary = violationsByDirective.get(directive)!;
summary.violation_count++;
});
// Calculer les statistiques détaillées pour chaque directive
for (const [directive, summary] of violationsByDirective.entries()) {
const directiveReports = reports.filter(r => r.violated_directive === directive);
// URIs bloquées uniques
const uniqueBlockedUris = new Set(directiveReports.map(r => r.blocked_uri || 'inline'));
summary.unique_blocked_uris = uniqueBlockedUris.size;
// Pages affectées uniques
const uniquePages = new Set(directiveReports.map(r => r.document_uri));
summary.affected_pages = uniquePages.size;
// Top 3 exemples avec comptage
const exampleCounts = new Map<string, { blocked_uri: string; document_uri: string; count: number }>();
directiveReports.forEach(r => {
const key = `${r.blocked_uri || 'inline'}|${r.document_uri}`;
if (!exampleCounts.has(key)) {
exampleCounts.set(key, {
blocked_uri: r.blocked_uri || 'inline',
document_uri: r.document_uri,
count: 0
});
}
exampleCounts.get(key)!.count++;
});
summary.examples = Array.from(exampleCounts.values())
.sort((a, b) => b.count - a.count)
.slice(0, 3);
}
const summaries = Array.from(violationsByDirective.values())
.sort((a, b) => b.violation_count - a.violation_count);
// Générer le contenu HTML du rapport
const reportHtml = generateReportHtml(summaries, reports.length);
// Envoyer l'email via le système universel
await sendUniversalEmailV2({
type: 'notification',
toEmail: 'paie@odentas.fr',
subject: `🔒 Rapport CSP quotidien - ${reports.length} violation${reports.length > 1 ? 's' : ''} détectée${reports.length > 1 ? 's' : ''}`,
data: {
firstName: 'Équipe',
organizationName: 'Odentas',
customMessage: reportHtml,
ctaUrl: 'https://espace-paie.odentas.fr/staff/csp-reports' // Pour future dashboard
}
});
// Logger l'envoi
await supabase
.from('csp_email_logs')
.insert({
report_date: today,
reports_count: reports.length,
unique_violations: summaries.length
});
console.log(`✅ Rapport CSP envoyé: ${reports.length} violations, ${summaries.length} directives`);
return NextResponse.json({
success: true,
violations_count: reports.length,
unique_directives: summaries.length,
summaries
});
} catch (error) {
console.error('❌ Erreur envoi rapport CSP:', error);
return NextResponse.json({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}, { status: 500 });
}
}
function generateReportHtml(summaries: ViolationSummary[], totalViolations: number): string {
let html = `
<div style="font-family: system-ui, -apple-system, sans-serif; padding: 20px; background-color: #f8fafc; border-radius: 8px;">
<h2 style="color: #1e293b; margin-top: 0;">Rapport CSP des dernières 24 heures</h2>
<p style="color: #64748b; margin-bottom: 24px;">
<strong>${totalViolations} violation${totalViolations > 1 ? 's' : ''}</strong> de Content Security Policy détectée${totalViolations > 1 ? 's' : ''}.
</p>
`;
summaries.forEach((summary, index) => {
html += `
<div style="background: white; padding: 16px; border-radius: 6px; margin-bottom: 16px; border-left: 4px solid ${index === 0 ? '#ef4444' : '#f59e0b'};">
<h3 style="color: #1e293b; margin-top: 0; font-size: 16px;">
${summary.violated_directive}
</h3>
<div style="color: #64748b; font-size: 14px; margin-bottom: 12px;">
<strong>${summary.violation_count}</strong> occurrence${summary.violation_count > 1 ? 's' : ''}
<strong>${summary.unique_blocked_uris}</strong> ressource${summary.unique_blocked_uris > 1 ? 's' : ''} bloquée${summary.unique_blocked_uris > 1 ? 's' : ''}
<strong>${summary.affected_pages}</strong> page${summary.affected_pages > 1 ? 's' : ''} affectée${summary.affected_pages > 1 ? 's' : ''}
</div>
<div style="background: #f8fafc; padding: 12px; border-radius: 4px; font-size: 13px;">
<strong style="color: #475569;">Exemples:</strong>
${summary.examples.map(ex => `
<div style="margin-top: 8px; padding: 8px; background: white; border-radius: 4px;">
<div style="color: #ef4444; font-family: monospace; font-size: 12px; word-break: break-all;">
${ex.blocked_uri}
</div>
<div style="color: #64748b; font-size: 11px; margin-top: 4px;">
Sur: ${ex.document_uri.replace('https://espace-paie.odentas.fr', '')}
${ex.count > 1 ? ` • <strong>${ex.count} fois</strong>` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`;
});
html += `
<div style="margin-top: 24px; padding: 16px; background: #eff6ff; border-radius: 6px; font-size: 13px; color: #1e40af;">
<strong>📋 Prochaines étapes:</strong>
<ol style="margin: 8px 0 0 0; padding-left: 20px;">
<li>Analyser les directives violées les plus fréquentes</li>
<li>Identifier si ce sont des ressources légitimes ou des tentatives d'attaque</li>
<li>Ajuster la CSP en conséquence (ajouter les sources autorisées)</li>
<li>Après 5 jours d'observation, passer en mode "Enforce"</li>
</ol>
</div>
<div style="margin-top: 16px; font-size: 12px; color: #94a3b8; text-align: center;">
Ce rapport est généré quotidiennement en mode Report-Only.<br>
Les violations sont collectées mais n'affectent pas le fonctionnement de l'application.
</div>
</div>
`;
return html;
}