Add processing activity exports

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-12-22 17:36:50 +01:00
parent 2a49c1e627
commit c76764b69a
29 changed files with 4021 additions and 226 deletions

View File

@@ -0,0 +1,315 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Protection Impact Assessments Export</title>
<style>
@page {
size: A4;
margin: 2.5cm;
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-family: Arial, sans-serif;
font-size: 9pt;
color: #666;
}
}
body {
font-family: Arial, sans-serif;
font-size: 7.5pt;
line-height: 1.4;
color: #333;
margin: 0;
padding: 0;
background: white;
}
/* Cover page */
.cover-page {
page-break-after: always;
}
.company-header {
margin-bottom: 30px;
page-break-after: avoid;
}
.company-logo {
max-height: 50px;
max-width: 250px;
object-fit: contain;
display: block;
}
.company-name {
font-size: 16pt;
color: #1a1a1a;
margin-top: 10px;
font-weight: 600;
}
.export-title {
font-size: 22pt;
font-weight: normal;
color: #1a1a1a;
margin: 0 0 25px 0;
text-align: left;
}
.document-meta {
margin: 0 0 30px 0;
font-size: 9pt;
}
.meta-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #333;
}
.meta-table td {
padding: 6px 8px;
border: 1px solid #333;
vertical-align: middle;
}
.meta-table td:first-child {
font-weight: 600;
width: 25%;
background: #f8f8f8;
color: #333;
}
.classification {
font-weight: bold;
text-transform: uppercase;
}
.purpose-section {
margin: 30px 0;
page-break-after: avoid;
}
.purpose-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.purpose-text {
font-size: 10pt;
color: #333;
line-height: 1.5;
text-align: justify;
}
/* Assessment page */
.assessment-page {
page-break-before: always;
page-break-after: always;
}
.assessment-page:last-child {
page-break-after: auto;
}
.assessment-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.assessment-field {
margin-bottom: 8px;
page-break-inside: avoid;
}
.assessment-field-label {
font-size: 10pt;
font-weight: bold;
color: #333;
margin: 12px 0 6px 0;
page-break-after: avoid;
page-break-inside: avoid;
}
.assessment-field-value {
color: #000;
font-size: 7.5pt;
line-height: 1.4;
}
.empty-value {
color: #999;
font-style: italic;
}
/* Annex page */
.annex-page {
page-break-before: always;
}
.annex-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.annex-section {
margin-bottom: 20px;
}
.annex-section-title {
font-size: 13pt;
font-weight: bold;
color: #000;
margin: 15px 0 10px 0;
}
.annex-subsection-title {
font-size: 10pt;
font-weight: bold;
color: #333;
margin: 12px 0 8px 0;
}
.annex-enum-list {
margin: 10px 0;
padding-left: 20px;
}
.annex-enum-item {
margin-bottom: 8px;
font-size: 10pt;
line-height: 1.5;
}
.annex-enum-name {
font-weight: 600;
color: #333;
}
.annex-enum-description {
color: #000;
margin-left: 5px;
}
@media print {
body {
background: white;
}
}
</style>
</head>
<body>
<div class="cover-page">
<div class="company-header">
{{- if .CompanyHorizontalLogoBase64}}
{{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}}
{{- else}}
<div class="company-name">{{.CompanyName}}</div>
{{- end}}
</div>
<h1 class="export-title">Data Protection Impact Assessments</h1>
<div class="document-meta">
<table class="meta-table">
<tr>
<td>Classification</td>
<td>
<span class="classification">CONFIDENTIAL</span>
</td>
</tr>
<tr>
<td>Version</td>
<td>{{.Version}}</td>
</tr>
<tr>
<td>Published</td>
<td>{{.PublishedAt.Format "January 2, 2006"}}</td>
</tr>
</table>
</div>
<div class="purpose-section">
<div class="purpose-title">1. Purpose</div>
<div class="purpose-text">
This document contains Data Protection Impact Assessments (DPIAs) conducted for processing activities
that present a high risk to individuals' rights and freedoms. DPIAs are systematic assessments that
evaluate the necessity, proportionality, and risks associated with data processing operations, along
with the measures implemented to mitigate identified risks.
</div>
</div>
</div>
{{- range $index, $assessment := .Assessments}}
<div class="assessment-page">
{{if eq $index 0}}
<h1 class="annex-title">2. Records</h1>
{{end}}
<h1 class="annex-section-title">2.{{add $index 1}} {{$assessment.ProcessingActivityName}}</h1>
<div class="assessment-field">
<div class="assessment-field-label">Description</div>
<div class="assessment-field-value">{{if .Description}}{{.Description}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="assessment-field">
<div class="assessment-field-label">Necessity and Proportionality</div>
<div class="assessment-field-value">{{if .NecessityAndProportionality}}{{.NecessityAndProportionality}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="assessment-field">
<div class="assessment-field-label">Potential Risk</div>
<div class="assessment-field-value">{{if .PotentialRisk}}{{.PotentialRisk}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="assessment-field">
<div class="assessment-field-label">Mitigations</div>
<div class="assessment-field-value">{{if .Mitigations}}{{.Mitigations}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="assessment-field">
<div class="assessment-field-label">Residual Risk</div>
<div class="assessment-field-value">{{if .ResidualRisk}}{{.ResidualRisk | formatResidualRisk}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
</div>
{{- end}}
<div class="annex-page">
<h1 class="annex-title">3. Annexes</h1>
<div class="annex-section">
<div class="annex-section-title">3.1 Lexicon</div>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Residual Risk</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Low:</span>
<span class="annex-enum-description">The residual risk after implementing mitigation measures is considered low. The processing activity poses minimal risk to individuals' rights and freedoms.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Medium:</span>
<span class="annex-enum-description">The residual risk after implementing mitigation measures is considered medium. The processing activity poses a moderate risk to individuals' rights and freedoms, requiring ongoing monitoring.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">High:</span>
<span class="annex-enum-description">The residual risk after implementing mitigation measures is considered high. The processing activity poses significant risk to individuals' rights and freedoms, requiring enhanced safeguards and regular review.</span>
</li>
</ul>
</div>
</div>
</body>
</html>

View File

@@ -23,21 +23,36 @@ import (
"strings"
"time"
"go.probo.inc/probo/pkg/coredata"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
gmhtml "github.com/yuin/goldmark/renderer/html"
"go.probo.inc/probo/pkg/coredata"
)
const (
firstPageTOCItems = 21
otherPageTOCItems = 28
)
var (
//go:embed template.html
htmlTemplateContent string
//go:embed processing_activities_template.html
processingActivitiesTemplateContent string
//go:embed data_protection_impact_assessments_template.html
dataProtectionImpactAssessmentsTemplateContent string
//go:embed transfer_impact_assessments_template.html
transferImpactAssessmentsTemplateContent string
templateFuncs = template.FuncMap{
"now": func() time.Time { return time.Now() },
"eq": func(a, b string) bool { return a == b },
"eq": func(a, b any) bool { return a == b },
"string": func(v fmt.Stringer) string { return v.String() },
"lower": func(s string) string { return strings.ToLower(s) },
"add": func(a, b int) int { return a + b },
"classificationString": func(c Classification) string { return string(c) },
"formatContent": func(content string) template.HTML {
md := goldmark.New(
@@ -56,9 +71,111 @@ var (
"imgTag": func(src, alt, class string) template.HTML {
return template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" class="%s">`, html.EscapeString(src), html.EscapeString(alt), html.EscapeString(class)))
},
"formatLawfulBasis": func(basis coredata.ProcessingActivityLawfulBasis) string {
switch basis {
case coredata.ProcessingActivityLawfulBasisConsent:
return "Consent"
case coredata.ProcessingActivityLawfulBasisContractualNecessity:
return "Contractual Necessity"
case coredata.ProcessingActivityLawfulBasisLegalObligation:
return "Legal Obligation"
case coredata.ProcessingActivityLawfulBasisLegitimateInterest:
return "Legitimate Interest"
case coredata.ProcessingActivityLawfulBasisPublicTask:
return "Public Task"
case coredata.ProcessingActivityLawfulBasisVitalInterests:
return "Vital Interests"
default:
return basis.String()
}
},
"formatSpecialOrCriminalData": func(data coredata.ProcessingActivitySpecialOrCriminalDatum) string {
switch data {
case coredata.ProcessingActivitySpecialOrCriminalDatumYes:
return "Yes"
case coredata.ProcessingActivitySpecialOrCriminalDatumNo:
return "No"
case coredata.ProcessingActivitySpecialOrCriminalDatumPossible:
return "Possible"
default:
return data.String()
}
},
"formatTransferSafeguard": func(safeguard *coredata.ProcessingActivityTransferSafeguard) string {
if safeguard == nil {
return ""
}
switch *safeguard {
case coredata.ProcessingActivityTransferSafeguardStandardContractualClauses:
return "Standard Contractual Clauses"
case coredata.ProcessingActivityTransferSafeguardBindingCorporateRules:
return "Binding Corporate Rules"
case coredata.ProcessingActivityTransferSafeguardAdequacyDecision:
return "Adequacy Decision"
case coredata.ProcessingActivityTransferSafeguardDerogations:
return "Derogations"
case coredata.ProcessingActivityTransferSafeguardCodesOfConduct:
return "Codes of Conduct"
case coredata.ProcessingActivityTransferSafeguardCertificationMechanisms:
return "Certification Mechanisms"
default:
return safeguard.String()
}
},
"formatDPIANeeded": func(needed coredata.ProcessingActivityDataProtectionImpactAssessment) string {
switch needed {
case coredata.ProcessingActivityDataProtectionImpactAssessmentNeeded:
return "Yes"
case coredata.ProcessingActivityDataProtectionImpactAssessmentNotNeeded:
return "No"
default:
return needed.String()
}
},
"formatTIANeeded": func(needed coredata.ProcessingActivityTransferImpactAssessment) string {
switch needed {
case coredata.ProcessingActivityTransferImpactAssessmentNeeded:
return "Yes"
case coredata.ProcessingActivityTransferImpactAssessmentNotNeeded:
return "No"
default:
return needed.String()
}
},
"formatRole": func(role coredata.ProcessingActivityRole) string {
switch role {
case coredata.ProcessingActivityRoleController:
return "Controller"
case coredata.ProcessingActivityRoleProcessor:
return "Processor"
default:
return role.String()
}
},
"formatResidualRisk": func(risk *coredata.DataProtectionImpactAssessmentResidualRisk) string {
if risk == nil {
return ""
}
switch *risk {
case coredata.DataProtectionImpactAssessmentResidualRiskLow:
return "Low"
case coredata.DataProtectionImpactAssessmentResidualRiskMedium:
return "Medium"
case coredata.DataProtectionImpactAssessmentResidualRiskHigh:
return "High"
default:
return risk.String()
}
},
}
documentTemplate = template.Must(template.New("document").Funcs(templateFuncs).Parse(htmlTemplateContent))
processingActivitiesTemplate = template.Must(template.New("processingActivities").Funcs(templateFuncs).Parse(processingActivitiesTemplateContent))
dataProtectionImpactAssessmentsTemplate = template.Must(template.New("dataProtectionImpactAssessments").Funcs(templateFuncs).Parse(dataProtectionImpactAssessmentsTemplateContent))
transferImpactAssessmentsTemplate = template.Must(template.New("transferImpactAssessments").Funcs(templateFuncs).Parse(transferImpactAssessmentsTemplateContent))
)
type (
@@ -82,6 +199,71 @@ type (
State coredata.DocumentVersionSignatureState
RequestedAt time.Time
}
ProcessingActivityTableData struct {
CompanyName string
CompanyHorizontalLogoBase64 string
Version int
PublishedAt time.Time
Activities []ProcessingActivityRowData
}
ProcessingActivityRowData struct {
Name string
Purpose *string
DataSubjectCategory *string
PersonalDataCategory *string
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalDatum
ConsentEvidenceLink *string
LawfulBasis coredata.ProcessingActivityLawfulBasis
Recipients *string
Location *string
InternationalTransfers bool
TransferSafeguards *coredata.ProcessingActivityTransferSafeguard
RetentionPeriod *string
SecurityMeasures *string
DataProtectionImpactAssessmentNeeded coredata.ProcessingActivityDataProtectionImpactAssessment
TransferImpactAssessmentNeeded coredata.ProcessingActivityTransferImpactAssessment
LastReviewDate *time.Time
NextReviewDate *time.Time
Role coredata.ProcessingActivityRole
DataProtectionOfficerFullName *string
Vendors string
}
DataProtectionImpactAssessmentTableData struct {
CompanyName string
CompanyHorizontalLogoBase64 string
Version int
PublishedAt time.Time
Assessments []DataProtectionImpactAssessmentRowData
}
DataProtectionImpactAssessmentRowData struct {
ProcessingActivityName string
Description *string
NecessityAndProportionality *string
PotentialRisk *string
Mitigations *string
ResidualRisk *coredata.DataProtectionImpactAssessmentResidualRisk
}
TransferImpactAssessmentTableData struct {
CompanyName string
CompanyHorizontalLogoBase64 string
Version int
PublishedAt time.Time
Assessments []TransferImpactAssessmentRowData
}
TransferImpactAssessmentRowData struct {
ProcessingActivityName string
DataSubjects *string
LegalMechanism *string
Transfer *string
LocalLawRisk *string
SupplementaryMeasures *string
}
)
const (
@@ -99,3 +281,30 @@ func RenderHTML(data DocumentData) ([]byte, error) {
return buf.Bytes(), nil
}
func RenderProcessingActivitiesTableHTML(data ProcessingActivityTableData) ([]byte, error) {
var buf bytes.Buffer
if err := processingActivitiesTemplate.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("cannot execute processing activities template: %w", err)
}
return buf.Bytes(), nil
}
func RenderDataProtectionImpactAssessmentsTableHTML(data DataProtectionImpactAssessmentTableData) ([]byte, error) {
var buf bytes.Buffer
if err := dataProtectionImpactAssessmentsTemplate.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("cannot execute data protection impact assessments template: %w", err)
}
return buf.Bytes(), nil
}
func RenderTransferImpactAssessmentsTableHTML(data TransferImpactAssessmentTableData) ([]byte, error) {
var buf bytes.Buffer
if err := transferImpactAssessmentsTemplate.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("cannot execute transfer impact assessments template: %w", err)
}
return buf.Bytes(), nil
}

View File

@@ -195,7 +195,7 @@ func TestTemplateFunctions(t *testing.T) {
})
t.Run("eq function", func(t *testing.T) {
eqFunc := templateFuncs["eq"].(func(string, string) bool)
eqFunc := templateFuncs["eq"].(func(any, any) bool)
assert.True(t, eqFunc("test", "test"))
assert.False(t, eqFunc("test", "other"))
})
@@ -266,7 +266,7 @@ func TestHTMLEscaping(t *testing.T) {
resultStr := string(result)
// Verify dangerous content is escaped
// Verify dangerous content is escaped
assert.NotContains(t, resultStr, "<script>alert('xss')</script>")
assert.NotContains(t, resultStr, "<malicious>tag")
assert.Contains(t, resultStr, "&lt;script&gt;")

View File

@@ -0,0 +1,535 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Processing Activities Export</title>
<style>
@page {
size: A4;
margin: 2.5cm;
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-family: Arial, sans-serif;
font-size: 9pt;
color: #666;
}
}
body {
font-family: Arial, sans-serif;
font-size: 7.5pt;
line-height: 1.4;
color: #333;
margin: 0;
padding: 0;
background: white;
}
/* Cover page */
.cover-page {
page-break-after: always;
}
.company-header {
margin-bottom: 30px;
page-break-after: avoid;
}
.company-logo {
max-height: 50px;
max-width: 250px;
object-fit: contain;
display: block;
}
.company-name {
font-size: 16pt;
color: #1a1a1a;
margin-top: 10px;
font-weight: 600;
}
.export-title {
font-size: 22pt;
font-weight: normal;
color: #1a1a1a;
margin: 0 0 25px 0;
text-align: left;
}
.document-meta {
margin: 0 0 30px 0;
font-size: 9pt;
}
.meta-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #333;
}
.meta-table td {
padding: 6px 8px;
border: 1px solid #333;
vertical-align: middle;
}
.meta-table td:first-child {
font-weight: 600;
width: 25%;
background: #f8f8f8;
color: #333;
}
.classification {
font-weight: bold;
text-transform: uppercase;
}
.purpose-section {
margin: 30px 0;
page-break-after: avoid;
}
.purpose-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.purpose-text {
font-size: 10pt;
color: #333;
line-height: 1.5;
text-align: justify;
}
/* Activity page */
.activity-page {
page-break-before: always;
page-break-after: always;
}
.activity-page:last-child {
page-break-after: auto;
}
.activity-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.activity-section {
margin-bottom: 8px;
page-break-inside: avoid;
}
.activity-section-title {
font-size: 10pt;
font-weight: bold;
color: #333;
margin: 12px 0 6px 0;
page-break-after: avoid;
page-break-inside: avoid;
}
.activity-field {
margin-bottom: 8px;
page-break-inside: avoid;
}
.activity-field-label {
font-weight: 600;
color: #333;
margin-bottom: 3px;
font-size: 7.5pt;
}
.activity-field-value {
color: #000;
font-size: 7.5pt;
line-height: 1.4;
}
.empty-value {
color: #999;
font-style: italic;
}
.activity-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 10px;
}
.activity-grid-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-bottom: 10px;
}
/* Annex page */
.annex-page {
page-break-before: always;
}
.annex-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.annex-section {
margin-bottom: 20px;
}
.annex-section-title {
font-size: 13pt;
font-weight: bold;
color: #000;
margin: 15px 0 10px 0;
}
.annex-subsection-title {
font-size: 10pt;
font-weight: bold;
color: #333;
margin: 12px 0 8px 0;
}
.annex-enum-list {
margin: 10px 0;
padding-left: 20px;
}
.annex-enum-item {
margin-bottom: 8px;
font-size: 10pt;
line-height: 1.5;
}
.annex-enum-name {
font-weight: 600;
color: #333;
}
.annex-enum-description {
color: #000;
margin-left: 5px;
}
@media print {
body {
background: white;
}
}
</style>
</head>
<body>
<div class="cover-page">
<div class="company-header">
{{- if .CompanyHorizontalLogoBase64}}
{{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}}
{{- else}}
<div class="company-name">{{.CompanyName}}</div>
{{- end}}
</div>
<h1 class="export-title">Processing Activities</h1>
<div class="document-meta">
<table class="meta-table">
<tr>
<td>Classification</td>
<td>
<span class="classification">CONFIDENTIAL</span>
</td>
</tr>
<tr>
<td>Version</td>
<td>{{.Version}}</td>
</tr>
<tr>
<td>Published</td>
<td>{{.PublishedAt.Format "January 2, 2006"}}</td>
</tr>
</table>
</div>
<div class="purpose-section">
<div class="purpose-title">1. Purpose</div>
<div class="purpose-text">
This document provides a comprehensive overview of all processing activities within the organization.
It serves as a record of personal data processing operations, documenting the purposes, legal bases,
data categories, and associated safeguards for each activity.
</div>
</div>
</div>
{{- range $index, $activity := .Activities}}
<div class="activity-page">
{{if eq $index 0}}
<h1 class="annex-title">2. Records</h1>
{{end}}
<h1 class="annex-section-title">2.{{add $index 1}} {{$activity.Name}}</h1>
<div class="activity-section">
<div class="activity-section-title">2.{{add $index 1}}.1 General Information</div>
<div class="activity-field">
<div class="activity-field-label">Purpose</div>
<div class="activity-field-value">{{if .Purpose}}{{.Purpose}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="activity-grid">
<div class="activity-field">
<div class="activity-field-label">Role</div>
<div class="activity-field-value">{{.Role | formatRole}}</div>
</div>
</div>
</div>
<div class="activity-section">
<div class="activity-section-title">2.{{add $index 1}}.2 Data Categories</div>
<div class="activity-grid-3">
<div class="activity-field">
<div class="activity-field-label">Data Subject Category</div>
<div class="activity-field-value">{{if .DataSubjectCategory}}{{.DataSubjectCategory}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Personal Data Category</div>
<div class="activity-field-value">{{if .PersonalDataCategory}}{{.PersonalDataCategory}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Special/Criminal Data</div>
<div class="activity-field-value">{{.SpecialOrCriminalData | formatSpecialOrCriminalData}}</div>
</div>
</div>
</div>
<div class="activity-section">
<div class="activity-section-title">2.{{add $index 1}}.3 Legal Basis</div>
<div class="activity-grid">
<div class="activity-field">
<div class="activity-field-label">Lawful Basis</div>
<div class="activity-field-value">{{.LawfulBasis | formatLawfulBasis}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Consent Evidence Link</div>
<div class="activity-field-value">{{if .ConsentEvidenceLink}}{{.ConsentEvidenceLink}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
</div>
</div>
<div class="activity-section">
<div class="activity-section-title">2.{{add $index 1}}.4 Data Sharing & Transfers</div>
<div class="activity-grid">
<div class="activity-field">
<div class="activity-field-label">Recipients</div>
<div class="activity-field-value">{{if .Recipients}}{{.Recipients}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Location</div>
<div class="activity-field-value">{{if .Location}}{{.Location}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">International Transfers</div>
<div class="activity-field-value">{{if .InternationalTransfers}}Yes{{else}}No{{end}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Transfer Safeguards</div>
<div class="activity-field-value">{{if .TransferSafeguards}}{{.TransferSafeguards | formatTransferSafeguard}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
</div>
</div>
<div class="activity-section">
<div class="activity-section-title">2.{{add $index 1}}.5 Retention & Security</div>
<div class="activity-grid">
<div class="activity-field">
<div class="activity-field-label">Retention Period</div>
<div class="activity-field-value">{{if .RetentionPeriod}}{{.RetentionPeriod}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Security Measures</div>
<div class="activity-field-value">{{if .SecurityMeasures}}{{.SecurityMeasures}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
</div>
<div class="activity-section">
<div class="activity-section-title">2.{{add $index 1}}.6 Assessments & Reviews</div>
<div class="activity-grid">
<div class="activity-field">
<div class="activity-field-label">DPIA Needed</div>
<div class="activity-field-value">{{.DataProtectionImpactAssessmentNeeded | formatDPIANeeded}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">TIA Needed</div>
<div class="activity-field-value">{{.TransferImpactAssessmentNeeded | formatTIANeeded}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Last Review Date</div>
<div class="activity-field-value">{{if .LastReviewDate}}{{.LastReviewDate.Format "January 2, 2006"}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Next Review Date</div>
<div class="activity-field-value">{{if .NextReviewDate}}{{.NextReviewDate.Format "January 2, 2006"}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
</div>
</div>
<div class="activity-section">
<div class="activity-section-title">2.{{add $index 1}}.7 Responsible Parties</div>
<div class="activity-grid">
<div class="activity-field">
<div class="activity-field-label">Data Protection Officer</div>
<div class="activity-field-value">{{if .DataProtectionOfficerFullName}}{{.DataProtectionOfficerFullName}}{{else}}<span class="empty-value">Not assigned</span>{{end}}</div>
</div>
<div class="activity-field">
<div class="activity-field-label">Vendors</div>
<div class="activity-field-value">{{if .Vendors}}{{.Vendors}}{{else}}<span class="empty-value">None</span>{{end}}</div>
</div>
</div>
</div>
</div>
{{- end}}
<div class="annex-page">
<h1 class="annex-title">3. Annexes</h1>
<div class="annex-section">
<div class="annex-section-title">3.1 Lexicon</div>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Role</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Controller:</span>
<span class="annex-enum-description">The entity that determines the purposes and means of processing personal data.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Processor:</span>
<span class="annex-enum-description">The entity that processes personal data on behalf of the controller.</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Lawful Basis</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Consent:</span>
<span class="annex-enum-description">The data subject has given consent to the processing of their personal data.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Contractual Necessity:</span>
<span class="annex-enum-description">Processing is necessary for the performance of a contract to which the data subject is party.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Legal Obligation:</span>
<span class="annex-enum-description">Processing is necessary for compliance with a legal obligation to which the controller is subject.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Legitimate Interest:</span>
<span class="annex-enum-description">Processing is necessary for the purposes of the legitimate interests pursued by the controller or a third party.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Public Task:</span>
<span class="annex-enum-description">Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Vital Interests:</span>
<span class="annex-enum-description">Processing is necessary to protect the vital interests of the data subject or of another natural person.</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Special/Criminal Data</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">The processing activity involves special categories of personal data or data relating to criminal convictions and offences.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">The processing activity does not involve special categories of personal data or data relating to criminal convictions and offences.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Possible:</span>
<span class="annex-enum-description">The processing activity may involve special categories of personal data or data relating to criminal convictions and offences.</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Transfer Safeguards</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Standard Contractual Clauses:</span>
<span class="annex-enum-description">Standard contractual clauses approved by the European Commission are used to ensure adequate protection for international transfers.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Binding Corporate Rules:</span>
<span class="annex-enum-description">Binding corporate rules approved by a supervisory authority are used to ensure adequate protection for international transfers.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Adequacy Decision:</span>
<span class="annex-enum-description">The European Commission has determined that the third country ensures an adequate level of protection.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Derogations:</span>
<span class="annex-enum-description">A derogation under Article 49 of the GDPR is used for the international transfer.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Codes of Conduct:</span>
<span class="annex-enum-description">An approved code of conduct together with binding and enforceable commitments is used to ensure adequate protection.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">Certification Mechanisms:</span>
<span class="annex-enum-description">An approved certification mechanism together with binding and enforceable commitments is used to ensure adequate protection.</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">DPIA Needed</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">A Data Protection Impact Assessment is required for this processing activity.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">A Data Protection Impact Assessment is not required for this processing activity.</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">TIA Needed</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">A Transfer Impact Assessment is required for this processing activity.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">A Transfer Impact Assessment is not required for this processing activity.</span>
</li>
</ul>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transfer Impact Assessments Export</title>
<style>
@page {
size: A4;
margin: 2.5cm;
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-family: Arial, sans-serif;
font-size: 9pt;
color: #666;
}
}
body {
font-family: Arial, sans-serif;
font-size: 7.5pt;
line-height: 1.4;
color: #333;
margin: 0;
padding: 0;
background: white;
}
/* Cover page */
.cover-page {
page-break-after: always;
}
.company-header {
margin-bottom: 30px;
page-break-after: avoid;
}
.company-logo {
max-height: 50px;
max-width: 250px;
object-fit: contain;
display: block;
}
.company-name {
font-size: 16pt;
color: #1a1a1a;
margin-top: 10px;
font-weight: 600;
}
.export-title {
font-size: 22pt;
font-weight: normal;
color: #1a1a1a;
margin: 0 0 25px 0;
text-align: left;
}
.document-meta {
margin: 0 0 30px 0;
font-size: 9pt;
}
.meta-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #333;
}
.meta-table td {
padding: 6px 8px;
border: 1px solid #333;
vertical-align: middle;
}
.meta-table td:first-child {
font-weight: 600;
width: 25%;
background: #f8f8f8;
color: #333;
}
.classification {
font-weight: bold;
text-transform: uppercase;
}
.purpose-section {
margin: 30px 0;
page-break-after: avoid;
}
.purpose-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.purpose-text {
font-size: 10pt;
color: #333;
line-height: 1.5;
text-align: justify;
}
/* Assessment page */
.assessment-page {
page-break-before: always;
page-break-after: always;
}
.assessment-page:last-child {
page-break-after: auto;
}
.assessment-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.assessment-field {
margin-bottom: 8px;
page-break-inside: avoid;
}
.assessment-field-label {
font-size: 10pt;
font-weight: bold;
color: #333;
margin: 12px 0 6px 0;
page-break-after: avoid;
page-break-inside: avoid;
}
.assessment-field-value {
color: #000;
font-size: 7.5pt;
line-height: 1.4;
}
.empty-value {
color: #999;
font-style: italic;
}
.annex-page {
page-break-before: always;
}
.annex-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.annex-section {
margin-bottom: 20px;
}
.annex-section-title {
font-size: 13pt;
font-weight: bold;
color: #000;
margin: 15px 0 10px 0;
}
.annex-subsection-title {
font-size: 10pt;
font-weight: bold;
color: #333;
margin: 12px 0 8px 0;
}
.annex-enum-list {
margin: 10px 0;
padding-left: 20px;
}
.annex-enum-item {
margin-bottom: 8px;
font-size: 10pt;
line-height: 1.5;
}
.annex-enum-name {
font-weight: 600;
color: #333;
}
.annex-enum-description {
color: #000;
margin-left: 5px;
}
@media print {
body {
background: white;
}
}
</style>
</head>
<body>
<div class="cover-page">
<div class="company-header">
{{- if .CompanyHorizontalLogoBase64}}
{{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}}
{{- else}}
<div class="company-name">{{.CompanyName}}</div>
{{- end}}
</div>
<h1 class="export-title">Transfer Impact Assessments</h1>
<div class="document-meta">
<table class="meta-table">
<tr>
<td>Classification</td>
<td>
<span class="classification">CONFIDENTIAL</span>
</td>
</tr>
<tr>
<td>Version</td>
<td>{{.Version}}</td>
</tr>
<tr>
<td>Published</td>
<td>{{.PublishedAt.Format "January 2, 2006"}}</td>
</tr>
</table>
</div>
<div class="purpose-section">
<div class="purpose-title">1. Purpose</div>
<div class="purpose-text">
This document contains Transfer Impact Assessments (TIAs) conducted for processing activities involving
international transfers of personal data to countries outside the European Economic Area (EEA). TIAs
evaluate the legal mechanisms used for transfers, assess risks related to local laws in destination
countries, and document supplementary measures implemented to ensure an adequate level of data protection.
</div>
</div>
</div>
{{- range $index, $assessment := .Assessments}}
<div class="assessment-page">
{{if eq $index 0}}
<h1 class="annex-title">2. Records</h1>
{{end}}
<h1 class="annex-section-title">2.{{add $index 1}} {{$assessment.ProcessingActivityName}}</h1>
<div class="assessment-field">
<div class="assessment-field-label">Data Subjects</div>
<div class="assessment-field-value">{{if .DataSubjects}}{{.DataSubjects}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="assessment-field">
<div class="assessment-field-label">Transfer</div>
<div class="assessment-field-value">{{if .Transfer}}{{.Transfer}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="assessment-field">
<div class="assessment-field-label">Legal Mechanism</div>
<div class="assessment-field-value">{{if .LegalMechanism}}{{.LegalMechanism}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="assessment-field">
<div class="assessment-field-label">Local Law Risk</div>
<div class="assessment-field-value">{{if .LocalLawRisk}}{{.LocalLawRisk}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
<div class="assessment-field">
<div class="assessment-field-label">Supplementary Measures</div>
<div class="assessment-field-value">{{if .SupplementaryMeasures}}{{.SupplementaryMeasures}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
</div>
</div>
{{- end}}
</body>
</html>