package compliance import ( "bytes" "fmt" "io" "strings" "time" "github.com/go-pdf/fpdf" ) // ─── colour palette ─────────────────────────────────────────────────────────── var ( colNavy = [3]int{30, 58, 95} colBlack = [3]int{30, 30, 30} colGray = [3]int{100, 100, 100} colLightBg = [3]int{245, 247, 250} colRed = [3]int{220, 38, 38} colOrange = [3]int{234, 88, 12} colAmber = [3]int{180, 110, 10} colGreen = [3]int{21, 128, 61} ) func riskColor(risk string) [3]int { switch risk { case "forbidden": return colRed case "high": return colOrange case "limited": return colAmber case "minimal": return colGreen default: return colGray } } // ─── helpers ───────────────────────────────────────────────────────────────── func newPDF() *fpdf.Fpdf { pdf := fpdf.New("P", "mm", "A4", "") pdf.SetMargins(20, 20, 20) pdf.SetAutoPageBreak(true, 20) return pdf } func setFont(pdf *fpdf.Fpdf, style string, size float64, col [3]int) { pdf.SetFont("Helvetica", style, size) pdf.SetTextColor(col[0], col[1], col[2]) } func sectionHeader(pdf *fpdf.Fpdf, title string) { pdf.Ln(6) pdf.SetFillColor(colNavy[0], colNavy[1], colNavy[2]) pdf.SetTextColor(255, 255, 255) pdf.SetFont("Helvetica", "B", 10) pdf.CellFormat(0, 8, " "+title, "", 1, "L", true, 0, "") pdf.SetTextColor(colBlack[0], colBlack[1], colBlack[2]) pdf.Ln(2) } func labelValue(pdf *fpdf.Fpdf, label, value string) { if value == "" { value = "—" } setFont(pdf, "B", 9, colGray) pdf.CellFormat(55, 6, label+":", "", 0, "L", false, 0, "") setFont(pdf, "", 9, colBlack) pdf.MultiCell(0, 6, value, "", "L", false) } func tableRow(pdf *fpdf.Fpdf, cols []string, widths []float64, fill bool) { if fill { pdf.SetFillColor(colLightBg[0], colLightBg[1], colLightBg[2]) } else { pdf.SetFillColor(255, 255, 255) } for i, col := range cols { pdf.CellFormat(widths[i], 6, col, "1", 0, "L", fill, 0, "") } pdf.Ln(-1) } func footer(pdf *fpdf.Fpdf) { pdf.SetFooterFunc(func() { pdf.SetY(-15) setFont(pdf, "I", 8, colGray) pdf.CellFormat(0, 10, fmt.Sprintf("Généré par Veylant IA · %s · Page %d/{nb}", time.Now().Format("02/01/2006"), pdf.PageNo(), ), "", 0, "C", false, 0, "") }) pdf.AliasNbPages("{nb}") } func covePage(pdf *fpdf.Fpdf, title, subtitle, tenantName string) { pdf.AddPage() pdf.Ln(30) // Title block pdf.SetFillColor(colNavy[0], colNavy[1], colNavy[2]) pdf.SetTextColor(255, 255, 255) pdf.SetFont("Helvetica", "B", 22) pdf.CellFormat(0, 18, title, "", 1, "C", true, 0, "") pdf.SetFont("Helvetica", "", 13) pdf.CellFormat(0, 10, subtitle, "", 1, "C", true, 0, "") pdf.Ln(6) // Tenant + date pdf.SetTextColor(colBlack[0], colBlack[1], colBlack[2]) pdf.SetFont("Helvetica", "", 11) pdf.CellFormat(0, 8, "Organisation : "+tenantName, "", 1, "C", false, 0, "") pdf.CellFormat(0, 8, "Date de génération : "+time.Now().Format("02 janvier 2006 à 15:04"), "", 1, "C", false, 0, "") pdf.Ln(10) // Confidential stamp pdf.SetFont("Helvetica", "B", 14) pdf.SetTextColor(colRed[0], colRed[1], colRed[2]) pdf.CellFormat(0, 10, "⚠ DOCUMENT CONFIDENTIEL", "", 1, "C", false, 0, "") } // ─── GenerateArticle30 ──────────────────────────────────────────────────────── // GenerateArticle30 generates a GDPR Article 30 processing registry PDF. func GenerateArticle30(entries []ProcessingEntry, tenantName string, w io.Writer) error { if tenantName == "" { tenantName = "Organisation" } pdf := newPDF() footer(pdf) covePage(pdf, "Registre des Activités de Traitement", "Conformément à l'Article 30 du Règlement (UE) 2016/679 (RGPD)", tenantName) // Section 1 — Responsable de traitement pdf.AddPage() sectionHeader(pdf, "1. Identification du Responsable de Traitement") pdf.Ln(2) labelValue(pdf, "Organisation", tenantName) labelValue(pdf, "Plateforme IA", "Veylant IA — Proxy IA multi-fournisseurs") labelValue(pdf, "DPO / Contact", "dpo@"+strings.ToLower(strings.ReplaceAll(tenantName, " ", ""))+".fr") labelValue(pdf, "Cadre réglementaire", "RGPD (UE) 2016/679, Loi Informatique et Libertés") // Section 2 — Tableau des traitements sectionHeader(pdf, "2. Activités de Traitement") pdf.Ln(2) if len(entries) == 0 { setFont(pdf, "I", 9, colGray) pdf.CellFormat(0, 8, "Aucun traitement enregistré.", "", 1, "L", false, 0, "") } else { widths := []float64{55, 40, 30, 40} headers := []string{"Cas d'usage", "Finalité", "Base légale", "Catégories de données"} setFont(pdf, "B", 9, colBlack) tableRow(pdf, headers, widths, true) for i, e := range entries { cats := strings.Join(e.DataCategories, ", ") if len(cats) > 35 { cats = cats[:32] + "..." } purpose := e.Purpose if len(purpose) > 38 { purpose = purpose[:35] + "..." } legalLabel := LegalBasisLabels[e.LegalBasis] if legalLabel == "" { legalLabel = e.LegalBasis } setFont(pdf, "", 8, colBlack) tableRow(pdf, []string{e.UseCaseName, purpose, legalLabel, cats}, widths, i%2 == 0) } } // Section 3 — Sous-traitants sectionHeader(pdf, "3. Destinataires et Sous-Traitants (Fournisseurs LLM)") pdf.Ln(2) allProcessors := map[string]bool{} for _, e := range entries { for _, p := range e.Processors { allProcessors[p] = true } for _, r := range e.Recipients { allProcessors[r] = true } } if len(allProcessors) == 0 { allProcessors["OpenAI (GPT-4o)"] = true allProcessors["Anthropic (Claude)"] = true } for proc := range allProcessors { setFont(pdf, "", 9, colBlack) pdf.CellFormat(5, 6, "•", "", 0, "L", false, 0, "") pdf.CellFormat(0, 6, proc+" — fournisseur LLM (sous-traitant au sens de l'Art. 28 RGPD)", "", 1, "L", false, 0, "") } // Section 4 — Durées de conservation sectionHeader(pdf, "4. Durées de Conservation") pdf.Ln(2) if len(entries) > 0 { widths := []float64{85, 80} headers := []string{"Cas d'usage", "Durée de conservation"} setFont(pdf, "B", 9, colBlack) tableRow(pdf, headers, widths, true) for i, e := range entries { setFont(pdf, "", 8, colBlack) tableRow(pdf, []string{e.UseCaseName, e.RetentionPeriod}, widths, i%2 == 0) } } pdf.Ln(3) setFont(pdf, "I", 8, colGray) pdf.MultiCell(0, 5, "Architecture Veylant IA : journaux chauds 90 jours (ClickHouse), archives tièdes 1 an, archives froides 5 ans (TTL automatique).", "", "L", false) // Section 5 — Mesures de sécurité sectionHeader(pdf, "5. Mesures de Sécurité Techniques et Organisationnelles") pdf.Ln(2) measures := []string{ "Chiffrement AES-256-GCM des prompts avant stockage", "Pseudonymisation automatique des données personnelles (PII) avant transmission aux LLM", "Contrôle d'accès RBAC (Admin, Manager, Utilisateur, Auditeur)", "Authentification forte via Keycloak (OIDC/SAML 2.0 / MFA)", "Journaux d'audit immuables (ClickHouse append-only, TTL uniquement)", "TLS 1.3 pour toutes les communications externes", "Circuit breaker pour la résilience des fournisseurs", "Séparation logique multi-locataires (Row-Level Security PostgreSQL)", } for _, m := range measures { setFont(pdf, "", 9, colBlack) pdf.CellFormat(5, 6, "✓", "", 0, "L", false, 0, "") pdf.MultiCell(0, 6, m, "", "L", false) } // Section 6 — Droits des personnes sectionHeader(pdf, "6. Droits des Personnes Concernées") pdf.Ln(2) rights := []struct{ art, desc string }{ {"Art. 15", "Droit d'accès — Endpoint GET /v1/admin/compliance/gdpr/access/{user_id}"}, {"Art. 16", "Droit de rectification — via l'interface d'administration"}, {"Art. 17", "Droit à l'effacement — Endpoint DELETE /v1/admin/compliance/gdpr/erase/{user_id}"}, {"Art. 18", "Droit à la limitation — contact DPO"}, {"Art. 20", "Droit à la portabilité — export JSON/CSV disponible"}, {"Art. 21", "Droit d'opposition — contact DPO"}, {"Art. 22", "Droit à ne pas faire l'objet d'une décision automatisée — supervision humaine obligatoire"}, } widths := []float64{20, 145} setFont(pdf, "B", 9, colBlack) tableRow(pdf, []string{"Article", "Description"}, widths, true) for i, r := range rights { setFont(pdf, "", 8, colBlack) tableRow(pdf, []string{r.art, r.desc}, widths, i%2 == 0) } var buf bytes.Buffer if err := pdf.Output(&buf); err != nil { return fmt.Errorf("pdf output: %w", err) } _, err := w.Write(buf.Bytes()) return err } // ─── GenerateAiActReport ────────────────────────────────────────────────────── // GenerateAiActReport generates an EU AI Act risk classification report PDF. func GenerateAiActReport(entries []ProcessingEntry, tenantName string, w io.Writer) error { if tenantName == "" { tenantName = "Organisation" } pdf := newPDF() footer(pdf) covePage(pdf, "Rapport de Classification AI Act", "Conformément au Règlement (UE) 2024/1689 sur l'Intelligence Artificielle", tenantName) pdf.AddPage() // Summary sectionHeader(pdf, "Synthèse de la Classification") pdf.Ln(2) counts := map[string]int{"forbidden": 0, "high": 0, "limited": 0, "minimal": 0, "": 0} for _, e := range entries { counts[e.RiskLevel]++ } widths := []float64{50, 30, 85} setFont(pdf, "B", 9, colBlack) tableRow(pdf, []string{"Niveau de risque", "Nb systèmes", "Obligations réglementaires"}, widths, true) obligations := map[string]string{ "forbidden": "INTERDIT — blocage automatique requis", "high": "DPIA obligatoire · supervision humaine · journalisation renforcée", "limited": "Obligation de transparence (Art. 50) · mention IA requise", "minimal": "Journalisation standard uniquement", "": "Non classifié — questionnaire à compléter", } riskOrder := []string{"forbidden", "high", "limited", "minimal", ""} for i, risk := range riskOrder { label := RiskLabels[risk] if label == "" { label = "Non classifié" } col := riskColor(risk) pdf.SetTextColor(col[0], col[1], col[2]) pdf.SetFont("Helvetica", "B", 8) fill := i%2 == 0 if fill { pdf.SetFillColor(colLightBg[0], colLightBg[1], colLightBg[2]) } else { pdf.SetFillColor(255, 255, 255) } pdf.CellFormat(widths[0], 6, label, "1", 0, "L", fill, 0, "") setFont(pdf, "", 8, colBlack) pdf.CellFormat(widths[1], 6, fmt.Sprintf("%d", counts[risk]), "1", 0, "C", fill, 0, "") pdf.CellFormat(widths[2], 6, obligations[risk], "1", 1, "L", fill, 0, "") } // Per-system detail if len(entries) > 0 { sectionHeader(pdf, "Détail par Système IA") pdf.Ln(2) for _, e := range entries { col := riskColor(e.RiskLevel) riskLabel := RiskLabels[e.RiskLevel] if riskLabel == "" { riskLabel = "Non classifié" } // System header pdf.SetFillColor(colLightBg[0], colLightBg[1], colLightBg[2]) pdf.SetFont("Helvetica", "B", 10) pdf.SetTextColor(colNavy[0], colNavy[1], colNavy[2]) pdf.CellFormat(0, 8, " "+e.UseCaseName, "LRT", 1, "L", true, 0, "") // Risk badge pdf.SetFont("Helvetica", "B", 9) pdf.SetTextColor(col[0], col[1], col[2]) pdf.CellFormat(40, 6, " Niveau : "+riskLabel, "LB", 0, "L", true, 0, "") setFont(pdf, "", 9, colBlack) pdf.CellFormat(0, 6, " Base légale : "+LegalBasisLabels[e.LegalBasis], "RB", 1, "L", true, 0, "") // Details pdf.Ln(1) labelValue(pdf, "Finalité", e.Purpose) labelValue(pdf, "Données traitées", strings.Join(e.DataCategories, ", ")) labelValue(pdf, "Durée conservation", e.RetentionPeriod) if len(e.AiActAnswers) > 0 { yesItems := []string{} for _, q := range AiActQuestions { if e.AiActAnswers[q.Key] { yesItems = append(yesItems, "• "+q.Label) } } if len(yesItems) > 0 { setFont(pdf, "B", 9, colGray) pdf.CellFormat(55, 6, "Critères AI Act :", "", 1, "L", false, 0, "") setFont(pdf, "", 8, colBlack) for _, yi := range yesItems { pdf.MultiCell(0, 5, " "+yi, "", "L", false) } } } pdf.Ln(4) } } // Regulatory note sectionHeader(pdf, "Note Réglementaire") pdf.Ln(2) setFont(pdf, "", 9, colBlack) pdf.MultiCell(0, 6, "Ce rapport est généré conformément au Règlement (UE) 2024/1689 sur l'Intelligence Artificielle (AI Act), "+ "entré en vigueur le 1er août 2024. Les systèmes classifiés \"Haut risque\" sont soumis à une évaluation "+ "de conformité avant déploiement. Les systèmes \"Interdits\" ne peuvent être mis en service sur le territoire "+ "de l'Union Européenne. Ce document doit être mis à jour à chaque modification substantielle d'un système IA.", "", "L", false) var buf bytes.Buffer if err := pdf.Output(&buf); err != nil { return fmt.Errorf("pdf output: %w", err) } _, err := w.Write(buf.Bytes()) return err } // ─── GenerateDPIA ───────────────────────────────────────────────────────────── // GenerateDPIA generates a pre-filled DPIA template for a processing entry (Art. 35 GDPR). func GenerateDPIA(entry ProcessingEntry, tenantName string, w io.Writer) error { if tenantName == "" { tenantName = "Organisation" } pdf := newPDF() footer(pdf) covePage(pdf, "Analyse d'Impact relative à la Protection des Données", "Data Protection Impact Assessment (DPIA) — Article 35 RGPD", tenantName) pdf.AddPage() // Section 1 — Description sectionHeader(pdf, "1. Description du Traitement") pdf.Ln(2) labelValue(pdf, "Cas d'usage", entry.UseCaseName) labelValue(pdf, "Finalité", entry.Purpose) labelValue(pdf, "Base légale", LegalBasisLabels[entry.LegalBasis]) labelValue(pdf, "Catégories de données", strings.Join(entry.DataCategories, ", ")) labelValue(pdf, "Destinataires", strings.Join(entry.Recipients, ", ")) labelValue(pdf, "Sous-traitants LLM", strings.Join(entry.Processors, ", ")) labelValue(pdf, "Durée de conservation", entry.RetentionPeriod) labelValue(pdf, "Classification AI Act", RiskLabels[entry.RiskLevel]) // Section 2 — Nécessité et proportionnalité sectionHeader(pdf, "2. Nécessité et Proportionnalité") pdf.Ln(2) setFont(pdf, "", 9, colBlack) pdf.MultiCell(0, 6, "Le traitement est nécessaire pour atteindre la finalité identifiée. "+ "La pseudonymisation automatique des données personnelles par Veylant IA "+ "(avant transmission aux fournisseurs LLM) constitue une mesure de minimisation des données "+ "conforme à l'Art. 5(1)(c) RGPD. "+ "Seules les catégories de données strictement nécessaires sont traitées.", "", "L", false) // Section 3 — Risques sectionHeader(pdf, "3. Évaluation des Risques") pdf.Ln(2) risks := []struct{ risk, proba, impact, mitigation string }{ { "Accès non autorisé aux données", "Faible", "Élevé", "RBAC strict, MFA, TLS 1.3, chiffrement AES-256-GCM", }, { "Fuite de données vers fournisseur LLM", "Très faible", "Élevé", "Pseudonymisation PII avant envoi, contrats DPA avec fournisseurs (Art. 28)", }, { "Rétention excessive des données", "Faible", "Moyen", "TTL automatique ClickHouse, politique de rétention définie (" + entry.RetentionPeriod + ")", }, { "Décision automatisée non supervisée", "Moyen", "Élevé", "Supervision humaine obligatoire pour décisions à impact légal", }, { "Indisponibilité du service", "Faible", "Moyen", "Circuit breaker, failover multi-fournisseurs, monitoring Prometheus", }, } widths := []float64{60, 22, 22, 61} setFont(pdf, "B", 9, colBlack) tableRow(pdf, []string{"Risque", "Probabilité", "Impact", "Mesure d'atténuation"}, widths, true) for i, r := range risks { setFont(pdf, "", 8, colBlack) tableRow(pdf, []string{r.risk, r.proba, r.impact, r.mitigation}, widths, i%2 == 0) } // Section 4 — Mesures d'atténuation sectionHeader(pdf, "4. Mesures d'Atténuation Implémentées") pdf.Ln(2) if entry.SecurityMeasures != "" { labelValue(pdf, "Mesures spécifiques", entry.SecurityMeasures) } genericMeasures := []string{ "Pseudonymisation automatique des PII (regex + NER + validation LLM)", "Chiffrement AES-256-GCM au repos et TLS 1.3 en transit", "RBAC avec 4 niveaux (Admin, Manager, Utilisateur, Auditeur)", "Journaux d'audit immuables avec conservation " + entry.RetentionPeriod, "Tests de sécurité SAST/DAST en pipeline CI/CD", "Contrats de sous-traitance (DPA) avec chaque fournisseur LLM", } for _, m := range genericMeasures { setFont(pdf, "", 9, colBlack) pdf.CellFormat(5, 6, "✓", "", 0, "L", false, 0, "") pdf.MultiCell(0, 6, m, "", "L", false) } // Section 5 — Risque résiduel sectionHeader(pdf, "5. Risque Résiduel et Conclusion") pdf.Ln(2) setFont(pdf, "", 9, colBlack) pdf.MultiCell(0, 6, "Après application des mesures d'atténuation identifiées, le risque résiduel est évalué comme "+ "ACCEPTABLE. Ce traitement peut être mis en œuvre sous réserve du respect continu des mesures "+ "de sécurité décrites. Une réévaluation annuelle ou lors de toute modification substantielle "+ "du traitement est recommandée.", "", "L", false) // Section 6 — Signatures sectionHeader(pdf, "6. Approbation") pdf.Ln(4) col1 := 85.0 col2 := 85.0 setFont(pdf, "B", 9, colBlack) pdf.CellFormat(col1, 6, "Responsable de traitement", "", 0, "C", false, 0, "") pdf.CellFormat(col2, 6, "Délégué à la Protection des Données", "", 1, "C", false, 0, "") pdf.Ln(10) setFont(pdf, "", 9, colGray) pdf.CellFormat(col1, 6, "Signature : ________________________", "", 0, "C", false, 0, "") pdf.CellFormat(col2, 6, "Signature : ________________________", "", 1, "C", false, 0, "") pdf.Ln(3) pdf.CellFormat(col1, 6, "Date : ____/____/________", "", 0, "C", false, 0, "") pdf.CellFormat(col2, 6, "Date : ____/____/________", "", 1, "C", false, 0, "") var buf bytes.Buffer if err := pdf.Output(&buf); err != nil { return fmt.Errorf("pdf output: %w", err) } _, err := w.Write(buf.Bytes()) return err }