autopv-cli
Version:
AutoPrivacy DSAR evidence-pack generator - Automated GDPR compliance for SaaS companies
308 lines (307 loc) • 10.7 kB
JavaScript
/**
* Evidence Pack Builder
* Generates PDF and CSV files for DSAR evidence packages
*/
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
import { Parser } from 'json2csv';
import { writeFileSync } from 'fs';
import { join } from 'path';
export class EvidencePackBuilder {
outputDir;
constructor(outputDir = '.') {
this.outputDir = outputDir;
}
/**
* Generate complete evidence pack (PDF + CSV)
*/
async generateEvidencePack(data) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const emailSafe = data.email.replace(/[^a-zA-Z0-9]/g, '_');
const pdfPath = join(this.outputDir, `evidence_${emailSafe}_${timestamp}.pdf`);
const csvPath = join(this.outputDir, `mapping_${emailSafe}_${timestamp}.csv`);
// Generate PDF
const pdfBuffer = await this.generatePDF(data);
writeFileSync(pdfPath, pdfBuffer);
// Generate CSV
const csvData = this.generateCSV(data);
writeFileSync(csvPath, csvData);
// Calculate summary statistics
const totalRecords = this.countRecords(data.scrubbed);
const gdprArticles = data.gdprClassification?.summary?.articlesFound || [];
return {
pdfPath,
csvPath,
filesCreated: [pdfPath, csvPath],
summary: {
pdfSize: pdfBuffer.length,
csvSize: csvData.length,
totalRecords,
gdprArticles
}
};
}
/**
* Generate PDF evidence document
*/
async generatePDF(data) {
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// Page 1: Cover Page
let page = pdfDoc.addPage([612, 792]); // US Letter size
let yPosition = 750;
// Title
page.drawText('DSAR EVIDENCE PACK', {
x: 50,
y: yPosition,
size: 24,
font: boldFont,
color: rgb(0, 0, 0.8)
});
yPosition -= 40;
// Subtitle
page.drawText(`Data Subject Access Request Evidence for ${data.email}`, {
x: 50,
y: yPosition,
size: 14,
font: font,
color: rgb(0.2, 0.2, 0.2)
});
yPosition -= 60;
// Export Information
const exportInfo = [
`Export Date: ${new Date(data.exportTimestamp).toLocaleString()}`,
`Data Subject: ${data.email}`,
`GitHub Organization: ${data.githubOrg || 'N/A'}`,
`Generated by: AutoPrivacy CLI`,
];
page.drawText('EXPORT INFORMATION', {
x: 50,
y: yPosition,
size: 16,
font: boldFont
});
yPosition -= 30;
for (const info of exportInfo) {
page.drawText(info, {
x: 70,
y: yPosition,
size: 12,
font: font
});
yPosition -= 20;
}
yPosition -= 30;
// Data Summary
page.drawText('DATA SUMMARY', {
x: 50,
y: yPosition,
size: 16,
font: boldFont
});
yPosition -= 30;
const dataSummary = [
`GitHub Events: ${data.scrubbed.github?.events?.length || 0}`,
`GitHub Audit Entries: ${data.scrubbed.github?.audit?.length || 0}`,
`Stripe Customers: ${data.scrubbed.stripe?.customers?.length || 0}`,
`Stripe Charges: ${data.scrubbed.stripe?.charges?.length || 0}`,
`Stripe Payment Methods: ${data.scrubbed.stripe?.methods?.length || 0}`,
];
for (const summary of dataSummary) {
page.drawText(summary, {
x: 70,
y: yPosition,
size: 12,
font: font
});
yPosition -= 20;
}
// PII Scrubbing Information
if (data.scrubStats) {
yPosition -= 30;
page.drawText('PII SCRUBBING REPORT', {
x: 50,
y: yPosition,
size: 16,
font: boldFont
});
yPosition -= 30;
const scrubInfo = [
`Emails Redacted: ${data.scrubStats.emailsFound}`,
`Phone Numbers Redacted: ${data.scrubStats.phonesFound}`,
`SSNs Redacted: ${data.scrubStats.ssnsFound}`,
`Credit Cards Redacted: ${data.scrubStats.creditCardsFound}`,
`API Keys Redacted: ${data.scrubStats.apiKeysFound}`,
`Data Size Reduction: ${data.scrubStats.totalReductions} bytes`,
];
for (const info of scrubInfo) {
page.drawText(info, {
x: 70,
y: yPosition,
size: 12,
font: font
});
yPosition -= 20;
}
}
// GDPR Classification
if (data.gdprClassification) {
yPosition -= 30;
page.drawText('GDPR COMPLIANCE ANALYSIS', {
x: 50,
y: yPosition,
size: 16,
font: boldFont
});
yPosition -= 30;
const gdprInfo = [
`Total Fields Analyzed: ${data.gdprClassification.summary.totalFields}`,
`GDPR Articles Identified: ${data.gdprClassification.summary.articlesFound.join(', ')}`,
`High Sensitivity Fields: ${data.gdprClassification.summary.highSensitivityFields}`,
`Processing Time: ${data.gdprClassification.summary.processingTime}ms`,
];
for (const info of gdprInfo) {
page.drawText(info, {
x: 70,
y: yPosition,
size: 12,
font: font
});
yPosition -= 20;
}
}
// Footer
page.drawText('This document contains personal data processed in accordance with GDPR Article 15.', {
x: 50,
y: 50,
size: 10,
font: font,
color: rgb(0.5, 0.5, 0.5)
});
// Page 2: Detailed Data (if needed)
if (yPosition < 200) {
page = pdfDoc.addPage([612, 792]);
yPosition = 750;
page.drawText('DETAILED DATA EXPORT', {
x: 50,
y: yPosition,
size: 18,
font: boldFont
});
yPosition -= 40;
// Add JSON data (truncated for readability)
const jsonData = JSON.stringify(data.scrubbed, null, 2);
const maxLength = 2000; // Limit for PDF readability
const truncatedData = jsonData.length > maxLength
? jsonData.substring(0, maxLength) + '\n\n... (truncated for readability, see CSV for complete data)'
: jsonData;
const lines = truncatedData.split('\n');
for (const line of lines) {
if (yPosition < 100)
break; // Avoid overflow
page.drawText(line.substring(0, 80), {
x: 50,
y: yPosition,
size: 8,
font: font
});
yPosition -= 12;
}
}
return pdfDoc.save();
}
/**
* Generate CSV mapping file
*/
generateCSV(data) {
const records = [];
// Flatten the data structure for CSV
this.flattenObject(data.scrubbed, '', records);
// Add GDPR classification if available
if (data.gdprClassification?.classifications) {
for (const classification of data.gdprClassification.classifications) {
const existingRecord = records.find(r => r.field === classification.field);
if (existingRecord) {
existingRecord.gdprArticle = classification.article;
existingRecord.dataType = classification.dataType;
existingRecord.sensitivity = classification.sensitivity;
existingRecord.reasoning = classification.reasoning;
}
}
}
// Define CSV fields
const fields = [
'field',
'value',
'type',
'gdprArticle',
'dataType',
'sensitivity',
'reasoning'
];
const parser = new Parser({ fields });
return parser.parse(records);
}
/**
* Flatten nested object for CSV export
*/
flattenObject(obj, prefix, records) {
if (obj === null || obj === undefined) {
records.push({
field: prefix || 'root',
value: obj,
type: typeof obj,
gdprArticle: '',
dataType: '',
sensitivity: '',
reasoning: ''
});
return;
}
if (typeof obj !== 'object') {
records.push({
field: prefix || 'root',
value: obj,
type: typeof obj,
gdprArticle: '',
dataType: '',
sensitivity: '',
reasoning: ''
});
return;
}
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
this.flattenObject(item, `${prefix}[${index}]`, records);
});
return;
}
for (const [key, value] of Object.entries(obj)) {
const fieldPath = prefix ? `${prefix}.${key}` : key;
this.flattenObject(value, fieldPath, records);
}
}
/**
* Count total records in the data structure
*/
countRecords(obj) {
if (obj === null || obj === undefined || typeof obj !== 'object') {
return 1;
}
if (Array.isArray(obj)) {
return obj.reduce((count, item) => count + this.countRecords(item), 0);
}
let totalCount = 0;
for (const value of Object.values(obj)) {
totalCount += this.countRecords(value);
}
return totalCount;
}
}
/**
* Convenience function to generate evidence pack
*/
export async function generateEvidencePack(data, outputDir) {
const builder = new EvidencePackBuilder(outputDir);
return builder.generateEvidencePack(data);
}