UNPKG

autopv-cli

Version:

AutoPrivacy DSAR evidence-pack generator - Automated GDPR compliance for SaaS companies

308 lines (307 loc) 10.7 kB
/** * 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); }