530 lines
18 KiB
Go
530 lines
18 KiB
Go
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
|
|
}
|