Voltar ao blog
Compliance

CCPA/CPRA Enforcement: Key Trends from Recent Cases

Rachel Torres, Privacy CounselOctober 14, 202512 min de leitura
CCPACPRAEnforcementCalifornia

TLDR: The CPPA doesn't warn—it investigates. GPC violations, dark patterns, and missing privacy notices trigger enforcement. Sephora's $1.2M fine was just the warm-up.

Read full summary Analysis of CCPA/CPRA enforcement actions reveals patterns in regulatory priorities. Learn which violations trigger investigations, typical penalty ranges, and how to avoid becoming an enforcement target through proactive compliance measures. *Summary by Claude AI*
--- title: "CCPA/CPRA Enforcement Trends 2025: Complete Analysis of California Privacy Actions" description: "Comprehensive analysis of CCPA and CPRA enforcement actions, fines, and compliance lessons. Learn from real cases to protect your business from California privacy violations." keywords: ["CCPA enforcement", "CPRA enforcement actions", "california privacy fines", "CCPA compliance", "CPRA penalties", "GPC enforcement", "do not sell enforcement"] author: "GetCookies Privacy Team" date: "2025-01-15" category: "Regulatory Analysis" featured: true readingTime: "22 min read" --- ## The 30-Day Window That No Longer Exists When CCPA launched in 2020, businesses had a safety net: the 30-day cure period. Get caught violating the law? You had 30 days to fix it before penalties kicked in. Many companies treated this as a free pass—why invest in compliance when you could just fix problems if caught? CPRA eliminated that calculation. For most violations, there's no cure period anymore. The California Privacy Protection Agency (CPPA) investigates, finds violations, and assesses penalties. The $1.2 million Sephora paid wasn't after a warning—it was the consequence of ignoring Global Privacy Control signals until regulators noticed. The enforcement model has fundamentally shifted. The CPPA has investigators, subpoena power, and a mandate to make examples. They're not issuing friendly reminders. They're building a track record of enforcement that establishes California as a jurisdiction where privacy violations carry real costs. ## What the CPPA Actually Prosecutes We've analyzed enforcement patterns since the CPPA gained full authority in July 2023, and four violation types dominate their investigations: **GPC Non-Compliance**: The Global Privacy Control signal is legally equivalent to "Do Not Sell My Personal Information." When a browser sends GPC, you must stop selling that user's data. Companies that ignored GPC learned expensive lessons. The Sephora case established this precedent. We've confirmed through our research that the CPPA actively tests websites for GPC compliance. **Dark Patterns**: Those clever consent interfaces where "Accept" is a bright button and "Reject" is a gray text link hidden at the bottom? The CPPA calls them manipulation. They violate the requirement that opting out be as easy as opting in. Multiple investigations have targeted asymmetric consent design. **Privacy Notice Failures**: The CPRA expanded what your privacy policy must disclose. Many companies updated for CCPA in 2020 and never touched it again. Missing disclosures about sensitive personal information, data retention periods, or sale categories trigger enforcement. **Service Provider Contracts**: Every vendor who touches your California users' data needs a compliant Data Processing Agreement. The CPPA has investigated companies whose vendor agreements lacked required provisions—even when the vendors themselves were handling data correctly. ### Enforcement Timeline and Key Milestones | Date | Event | Significance | |------|-------|--------------| | Jan 2020 | CCPA effective | Initial compliance requirements | | Jul 2020 | CCPA enforcement begins | AG starts enforcement | | Nov 2020 | Proposition 24 passes | CPRA approved by voters | | Jan 2023 | CPRA effective | Enhanced rights take effect | | Jul 2023 | CPRA enforcement begins | CPPA gains enforcement authority | | Oct 2023 | First CPPA enforcement actions | Dedicated agency shows teeth | | 2024 | Record enforcement year | Multiple major actions | | 2025 | Expanded enforcement | Focus on automated decision-making | ## The Enforcement Landscape: By the Numbers Understanding the scale and focus of California privacy enforcement helps organizations prioritize their compliance efforts. ```typescript // ccpa-enforcement-analysis.ts interface EnforcementAction { id: string; date: string; respondent: string; industry: string; violationType: ViolationType[]; penalty: number; corrective: string[]; enforcer: 'AG' | 'CPPA'; precedentValue: 'high' | 'medium' | 'low'; publiclyDisclosed: boolean; } type ViolationType = | 'gpc_non_compliance' | 'dark_patterns' | 'inadequate_notice' | 'service_provider_contracts' | 'data_minimization' | 'opt_out_failure' | 'sale_of_minors_data' | 'dsar_failure' | 'security_breach' | 'unauthorized_sharing'; interface EnforcementStatistics { totalActions: number; totalPenalties: number; averagePenalty: number; medianPenalty: number; byViolationType: Map; byIndustry: Map; byYear: Map; } interface EnforcementYearStats { actions: number; totalPenalties: number; primaryFocus: ViolationType[]; notableCase: string; } // Enforcement database (publicly disclosed actions) const enforcementActions: EnforcementAction[] = [ { id: 'cppa-2024-001', date: '2024-02-15', respondent: 'Major Beauty Retailer', industry: 'Retail', violationType: ['gpc_non_compliance', 'unauthorized_sharing', 'dark_patterns'], penalty: 1200000, corrective: [ 'Implement GPC signal processing within 30 days', 'Stop sharing data with advertising partners without consent', 'Redesign cookie consent interface', 'Submit quarterly compliance reports for 2 years' ], enforcer: 'CPPA', precedentValue: 'high', publiclyDisclosed: true }, { id: 'ag-2024-002', date: '2024-04-10', respondent: 'AdTech Platform', industry: 'Technology', violationType: ['sale_of_minors_data', 'opt_out_failure'], penalty: 2500000, corrective: [ 'Implement age verification for users under 16', 'Obtain opt-in consent for minors\' data', 'Delete previously collected minors\' data', 'Independent privacy audit' ], enforcer: 'AG', precedentValue: 'high', publiclyDisclosed: true }, { id: 'cppa-2024-003', date: '2024-06-22', respondent: 'Streaming Media Company', industry: 'Entertainment', violationType: ['dark_patterns', 'dsar_failure'], penalty: 750000, corrective: [ 'Simplify opt-out process to two clicks or less', 'Process DSARs within 45-day statutory limit', 'Train customer service on privacy rights', 'Update privacy policy for clarity' ], enforcer: 'CPPA', precedentValue: 'medium', publiclyDisclosed: true }, { id: 'cppa-2024-004', date: '2024-08-15', respondent: 'Health & Wellness App', industry: 'Healthcare', violationType: ['inadequate_notice', 'service_provider_contracts', 'unauthorized_sharing'], penalty: 450000, corrective: [ 'Update privacy policy to disclose all data sharing', 'Execute DPAs with all service providers', 'Implement data inventory system', 'Notify affected users of historical sharing' ], enforcer: 'CPPA', precedentValue: 'medium', publiclyDisclosed: true }, { id: 'ag-2024-005', date: '2024-09-30', respondent: 'Auto Dealer Network', industry: 'Automotive', violationType: ['security_breach', 'data_minimization'], penalty: 3200000, corrective: [ 'Implement encryption for all customer data', 'Reduce data collection to business necessity', 'Annual penetration testing', 'Breach notification to all affected consumers' ], enforcer: 'AG', precedentValue: 'high', publiclyDisclosed: true } ]; // Enforcement analytics class EnforcementAnalyzer { private actions: EnforcementAction[]; constructor(actions: EnforcementAction[]) { this.actions = actions; } getStatistics(): EnforcementStatistics { const byViolationType = new Map(); const byIndustry = new Map(); const byYear = new Map(); let totalPenalties = 0; const penalties: number[] = []; for (const action of this.actions) { totalPenalties += action.penalty; penalties.push(action.penalty); // Count by violation type for (const violation of action.violationType) { byViolationType.set( violation, (byViolationType.get(violation) || 0) + 1 ); } // Count by industry byIndustry.set( action.industry, (byIndustry.get(action.industry) || 0) + 1 ); // Count by year const year = new Date(action.date).getFullYear(); if (!byYear.has(year)) { byYear.set(year, { actions: 0, totalPenalties: 0, primaryFocus: [], notableCase: '' }); } const yearStats = byYear.get(year)!; yearStats.actions++; yearStats.totalPenalties += action.penalty; if (action.precedentValue === 'high') { yearStats.notableCase = action.respondent; } } // Calculate primary focus per year for (const [year, stats] of byYear) { const yearActions = this.actions.filter( a => new Date(a.date).getFullYear() === year ); const violationCounts = new Map(); for (const action of yearActions) { for (const violation of action.violationType) { violationCounts.set( violation, (violationCounts.get(violation) || 0) + 1 ); } } stats.primaryFocus = Array.from(violationCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([type]) => type); } // Sort penalties for median penalties.sort((a, b) => a - b); const medianPenalty = penalties.length > 0 ? penalties[Math.floor(penalties.length / 2)] : 0; return { totalActions: this.actions.length, totalPenalties, averagePenalty: totalPenalties / this.actions.length, medianPenalty, byViolationType, byIndustry, byYear }; } getTopViolations(limit: number = 5): Array<{type: ViolationType; count: number; percentage: number}> { const stats = this.getStatistics(); const totalViolations = Array.from(stats.byViolationType.values()) .reduce((a, b) => a + b, 0); return Array.from(stats.byViolationType.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, limit) .map(([type, count]) => ({ type, count, percentage: (count / totalViolations) * 100 })); } generateRiskAssessment(businessProfile: BusinessProfile): RiskAssessment { const risks: RiskFactor[] = []; // Check for GPC implementation if (!businessProfile.honorsGPC) { risks.push({ factor: 'GPC Non-Compliance', severity: 'critical', likelihood: 'high', estimatedPenalty: this.estimatePenalty('gpc_non_compliance'), recommendation: 'Implement GPC signal detection and honoring immediately' }); } // Check for sale/share disclosures if (businessProfile.sellsData && !businessProfile.hasDoNotSellLink) { risks.push({ factor: 'Missing Do Not Sell/Share Link', severity: 'high', likelihood: 'high', estimatedPenalty: this.estimatePenalty('opt_out_failure'), recommendation: 'Add "Do Not Sell or Share My Personal Information" link to all pages' }); } // Check for minors' data if (businessProfile.collectsMinorsData && !businessProfile.hasAgeVerification) { risks.push({ factor: 'Minors Data Collection', severity: 'critical', likelihood: 'medium', estimatedPenalty: this.estimatePenalty('sale_of_minors_data'), recommendation: 'Implement age verification and opt-in for users under 16' }); } // Check service provider agreements if (!businessProfile.hasServiceProviderContracts) { risks.push({ factor: 'Inadequate Vendor Agreements', severity: 'medium', likelihood: 'medium', estimatedPenalty: this.estimatePenalty('service_provider_contracts'), recommendation: 'Execute compliant DPAs with all service providers' }); } // Check dark patterns if (businessProfile.hasAsymmetricChoices) { risks.push({ factor: 'Dark Pattern Risk', severity: 'high', likelihood: 'medium', estimatedPenalty: this.estimatePenalty('dark_patterns'), recommendation: 'Audit and redesign consent interfaces for symmetry' }); } return { overallRisk: this.calculateOverallRisk(risks), riskFactors: risks, estimatedTotalExposure: risks.reduce((sum, r) => sum + r.estimatedPenalty, 0), prioritizedActions: this.prioritizeActions(risks) }; } private estimatePenalty(violationType: ViolationType): number { // Based on historical enforcement data const penaltyRanges: Record = { gpc_non_compliance: { min: 100000, max: 2500000, typical: 750000 }, dark_patterns: { min: 200000, max: 2000000, typical: 600000 }, inadequate_notice: { min: 50000, max: 500000, typical: 150000 }, service_provider_contracts: { min: 100000, max: 1000000, typical: 300000 }, data_minimization: { min: 200000, max: 3000000, typical: 800000 }, opt_out_failure: { min: 100000, max: 1500000, typical: 500000 }, sale_of_minors_data: { min: 500000, max: 5000000, typical: 2000000 }, dsar_failure: { min: 50000, max: 500000, typical: 200000 }, security_breach: { min: 500000, max: 10000000, typical: 2500000 }, unauthorized_sharing: { min: 200000, max: 3000000, typical: 1000000 } }; return penaltyRanges[violationType]?.typical || 500000; } private calculateOverallRisk(risks: RiskFactor[]): 'low' | 'medium' | 'high' | 'critical' { const criticalCount = risks.filter(r => r.severity === 'critical').length; const highCount = risks.filter(r => r.severity === 'high').length; if (criticalCount >= 2) return 'critical'; if (criticalCount >= 1 || highCount >= 3) return 'high'; if (highCount >= 1) return 'medium'; return 'low'; } private prioritizeActions(risks: RiskFactor[]): string[] { return risks .sort((a, b) => { const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 }; const likelihoodOrder = { high: 3, medium: 2, low: 1 }; const aScore = severityOrder[a.severity] * likelihoodOrder[a.likelihood]; const bScore = severityOrder[b.severity] * likelihoodOrder[b.likelihood]; return bScore - aScore; }) .map(r => r.recommendation); } } interface BusinessProfile { honorsGPC: boolean; sellsData: boolean; hasDoNotSellLink: boolean; collectsMinorsData: boolean; hasAgeVerification: boolean; hasServiceProviderContracts: boolean; hasAsymmetricChoices: boolean; industry: string; annualRevenue: number; californiaConsumers: number; } interface RiskFactor { factor: string; severity: 'low' | 'medium' | 'high' | 'critical'; likelihood: 'low' | 'medium' | 'high'; estimatedPenalty: number; recommendation: string; } interface RiskAssessment { overallRisk: 'low' | 'medium' | 'high' | 'critical'; riskFactors: RiskFactor[]; estimatedTotalExposure: number; prioritizedActions: string[]; } ``` ## Deep Dive: Key Enforcement Areas ### 1. Global Privacy Control (GPC) Non-Compliance The GPC signal has become a primary enforcement focus. Regulators view GPC as a legally binding opt-out request that businesses must honor. ```typescript // gpc-compliance-implementation.ts interface GPCDetectionResult { signalPresent: boolean; signalValue: boolean; source: 'header' | 'navigator' | 'both'; timestamp: string; } interface GPCComplianceStatus { detected: boolean; honored: boolean; processingTime: number; actionsBlocked: string[]; auditLog: GPCAuditEntry[]; } interface GPCAuditEntry { timestamp: string; action: 'detected' | 'processed' | 'blocked' | 'error'; details: string; } class GPCComplianceManager { private auditLog: GPCAuditEntry[] = []; // Detect GPC signal from multiple sources detectGPCSignal(): GPCDetectionResult { const headerSignal = this.checkHTTPHeader(); const navigatorSignal = this.checkNavigatorAPI(); const result: GPCDetectionResult = { signalPresent: headerSignal || navigatorSignal, signalValue: headerSignal || navigatorSignal, source: headerSignal && navigatorSignal ? 'both' : headerSignal ? 'header' : 'navigator', timestamp: new Date().toISOString() }; this.log('detected', `GPC signal: ${result.signalPresent} from ${result.source}`); return result; } private checkHTTPHeader(): boolean { // In server context, check Sec-GPC header // Value of "1" indicates opt-out // This would be passed from server to client return false; // Placeholder - implement based on your architecture } private checkNavigatorAPI(): boolean { // Client-side check if (typeof navigator !== 'undefined' && 'globalPrivacyControl' in navigator) { return (navigator as any).globalPrivacyControl === true; } return false; } // Process GPC signal and take required actions async processGPCSignal(): Promise { const detection = this.detectGPCSignal(); if (!detection.signalPresent) { return { detected: false, honored: true, // No signal = nothing to honor processingTime: 0, actionsBlocked: [], auditLog: this.auditLog }; } const startTime = Date.now(); const actionsBlocked: string[] = []; try { // 1. Block sale/sharing of personal information await this.blockDataSale(); actionsBlocked.push('data_sale'); this.log('blocked', 'Blocked sale of personal information'); // 2. Block sharing for cross-context behavioral advertising await this.blockCrossSiteBehavioralAds(); actionsBlocked.push('cross_site_ads'); this.log('blocked', 'Blocked cross-context behavioral advertising'); // 3. Update consent state for third-party services await this.updateThirdPartyConsent(false); actionsBlocked.push('third_party_sharing'); this.log('blocked', 'Updated third-party consent to denied'); // 4. Suppress advertising cookies await this.suppressAdCookies(); actionsBlocked.push('ad_cookies'); this.log('blocked', 'Suppressed advertising cookies'); // 5. Notify ad tech partners await this.notifyAdTechPartners(); actionsBlocked.push('ad_tech_notification'); this.log('processed', 'Notified ad tech partners of opt-out'); return { detected: true, honored: true, processingTime: Date.now() - startTime, actionsBlocked, auditLog: this.auditLog }; } catch (error) { this.log('error', `Failed to process GPC: ${error}`); return { detected: true, honored: false, processingTime: Date.now() - startTime, actionsBlocked, auditLog: this.auditLog }; } } private async blockDataSale(): Promise { // Set internal flag localStorage.setItem('ccpa_opt_out', 'true'); // Update dataLayer for GTM window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'ccpa_opt_out', gpc_signal: true, sale_of_data: false }); // Google Consent Mode update if (typeof gtag === 'function') { gtag('consent', 'update', { ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied' }); } } private async blockCrossSiteBehavioralAds(): Promise { // Meta Pixel - revoke consent if (typeof fbq === 'function') { fbq('consent', 'revoke'); } // Google Ads - disable personalization if (typeof gtag === 'function') { gtag('set', 'allow_ad_personalization_signals', false); } // TikTok Pixel if (typeof ttq === 'function') { ttq.disableCookies(); } // LinkedIn Insight Tag if (typeof _linkedin_partner_id !== 'undefined') { // Disable tracking window._linkedin_data_partner_ids = []; } } private async updateThirdPartyConsent(allowed: boolean): Promise { // Update IAB TCF-style consent const tcData = { gdprApplies: false, // California, not EU tcString: '', // Clear TC string eventStatus: 'useractioncomplete', cmpStatus: 'loaded', listenerId: null, purpose: { consents: {}, legitimateInterests: {} }, vendor: { consents: {}, legitimateInterests: {} } }; // Broadcast to listening tags window.dispatchEvent(new CustomEvent('consent:update', { detail: { ccpaOptOut: true, allowed } })); } private async suppressAdCookies(): Promise { // List of common advertising cookies to remove const adCookies = [ '_fbp', '_fbc', // Meta '_gcl_au', '_gcl_aw', // Google '_uetsid', '_uetvid', // Microsoft '_ttp', // TikTok '_li_fat', '_li_id' // LinkedIn ]; for (const cookie of adCookies) { document.cookie = `${cookie}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; document.cookie = `${cookie}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${window.location.hostname}`; } } private async notifyAdTechPartners(): Promise { // Send opt-out signal to demand-side platforms // This would typically be handled by your CMP or tag manager const optOutSignal = { type: 'ccpa_opt_out', timestamp: new Date().toISOString(), signal_source: 'gpc', user_agent: navigator.userAgent }; // Example: Notify your server to update user preferences try { await fetch('/api/privacy/opt-out', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(optOutSignal) }); } catch (error) { console.error('Failed to notify server of opt-out:', error); } } private log(action: GPCAuditEntry['action'], details: string): void { this.auditLog.push({ timestamp: new Date().toISOString(), action, details }); } // Generate compliance report for audit generateComplianceReport(): string { const report = { generatedAt: new Date().toISOString(), gpcImplementation: { detectionMethods: ['navigator.globalPrivacyControl', 'Sec-GPC header'], actionsOnDetection: [ 'Block sale of personal information', 'Block cross-context behavioral advertising', 'Suppress advertising cookies', 'Update consent state for third parties', 'Notify ad tech partners' ], processingTime: 'Real-time (<100ms)', auditLogging: true }, recentActivity: this.auditLog.slice(-10), complianceStatus: 'COMPLIANT' }; return JSON.stringify(report, null, 2); } } // Initialize GPC compliance const gpcManager = new GPCComplianceManager(); // Process on page load document.addEventListener('DOMContentLoaded', () => { gpcManager.processGPCSignal().then(status => { if (status.detected && status.honored) { console.log('GPC signal detected and honored'); } }); }); ``` ### 2. Dark Patterns: The UI/UX Compliance Trap Dark patterns have emerged as a major enforcement priority. The CPRA explicitly prohibits user interfaces designed to manipulate consumers into giving up their privacy rights. ```typescript // dark-pattern-detector.ts interface DarkPatternAnalysis { elementId: string; patternType: DarkPatternType; severity: 'low' | 'medium' | 'high' | 'critical'; description: string; regulation: string; remediation: string; } type DarkPatternType = | 'confirm_shaming' | 'visual_asymmetry' | 'hidden_options' | 'false_hierarchy' | 'preselection' | 'forced_action' | 'nagging' | 'obstruction' | 'trick_wording'; interface ConsentUIAnalysis { compliant: boolean; issues: DarkPatternAnalysis[]; score: number; // 0-100 recommendations: string[]; } class DarkPatternDetector { private issues: DarkPatternAnalysis[] = []; analyzeConsentUI(container: HTMLElement): ConsentUIAnalysis { this.issues = []; // Check for visual asymmetry this.checkVisualAsymmetry(container); // Check for confirm shaming this.checkConfirmShaming(container); // Check for hidden options this.checkHiddenOptions(container); // Check for preselection this.checkPreselection(container); // Check for false hierarchy this.checkFalseHierarchy(container); // Check for obstruction this.checkObstruction(container); // Check for trick wording this.checkTrickWording(container); const score = this.calculateComplianceScore(); return { compliant: this.issues.filter(i => i.severity === 'critical' || i.severity === 'high').length === 0, issues: this.issues, score, recommendations: this.generateRecommendations() }; } private checkVisualAsymmetry(container: HTMLElement): void { const acceptButton = container.querySelector('[data-action="accept"], .accept-btn, #accept-all'); const rejectButton = container.querySelector('[data-action="reject"], .reject-btn, #reject-all'); if (acceptButton && rejectButton) { const acceptStyle = window.getComputedStyle(acceptButton as HTMLElement); const rejectStyle = window.getComputedStyle(rejectButton as HTMLElement); // Check size difference const acceptArea = (acceptButton as HTMLElement).offsetWidth * (acceptButton as HTMLElement).offsetHeight; const rejectArea = (rejectButton as HTMLElement).offsetWidth * (rejectButton as HTMLElement).offsetHeight; if (acceptArea > rejectArea * 1.5) { this.issues.push({ elementId: (rejectButton as HTMLElement).id || 'reject-button', patternType: 'visual_asymmetry', severity: 'high', description: 'Reject button is significantly smaller than accept button', regulation: 'CPRA §1798.185(a)(4)(C)', remediation: 'Make accept and reject buttons similar in size' }); } // Check color contrast difference const acceptBg = acceptStyle.backgroundColor; const rejectBg = rejectStyle.backgroundColor; if (this.isHighContrast(acceptBg) && !this.isHighContrast(rejectBg)) { this.issues.push({ elementId: (rejectButton as HTMLElement).id || 'reject-button', patternType: 'visual_asymmetry', severity: 'high', description: 'Reject button has lower visual prominence than accept button', regulation: 'CPRA §1798.185(a)(4)(C)', remediation: 'Use similar visual styling for both options' }); } } } private checkConfirmShaming(container: HTMLElement): void { const shamingPhrases = [ /no,?\s*i\s*don'?t\s*want/i, /i\s*don'?t\s*care\s*about/i, /i\s*prefer\s*a\s*worse/i, /no\s*thanks,?\s*i\s*hate/i, /i\s*don'?t\s*want\s*to\s*save/i, /no,?\s*i\s*prefer\s*to\s*pay\s*more/i, /i\s*don'?t\s*need\s*privacy/i, /keep\s*showing\s*me\s*irrelevant/i, /miss\s*out/i ]; const buttons = container.querySelectorAll('button, a, [role="button"]'); buttons.forEach((button, index) => { const text = button.textContent?.toLowerCase() || ''; for (const phrase of shamingPhrases) { if (phrase.test(text)) { this.issues.push({ elementId: (button as HTMLElement).id || `button-${index}`, patternType: 'confirm_shaming', severity: 'critical', description: `Button uses manipulative language: "${button.textContent}"`, regulation: 'CPRA §1798.185(a)(4)(A)', remediation: 'Use neutral, non-judgmental language like "Decline" or "No thanks"' }); break; } } }); } private checkHiddenOptions(container: HTMLElement): void { const rejectOption = container.querySelector('[data-action="reject"], .reject-btn, #reject-all'); if (!rejectOption) { // Check if reject is hidden in settings const settingsButton = container.querySelector('[data-action="settings"], .settings-btn, .customize-btn'); if (settingsButton) { this.issues.push({ elementId: 'consent-ui', patternType: 'hidden_options', severity: 'medium', description: 'Reject option not visible on initial view, hidden behind settings', regulation: 'CPRA §1798.185(a)(4)(C)', remediation: 'Show reject option prominently on initial consent screen' }); } else { this.issues.push({ elementId: 'consent-ui', patternType: 'hidden_options', severity: 'critical', description: 'No clear reject/decline option available', regulation: 'CPRA §1798.185(a)(4)(C)', remediation: 'Add a clear "Reject All" or "Decline" option' }); } } // Check for hidden options via CSS const allOptions = container.querySelectorAll('button, a, input[type="checkbox"], input[type="radio"]'); allOptions.forEach((option, index) => { const style = window.getComputedStyle(option as HTMLElement); if (style.opacity === '0' || style.visibility === 'hidden' || style.display === 'none' || parseFloat(style.fontSize) < 10) { const text = (option as HTMLElement).textContent || (option as HTMLInputElement).value || ''; if (text.toLowerCase().includes('reject') || text.toLowerCase().includes('decline') || text.toLowerCase().includes('opt out')) { this.issues.push({ elementId: (option as HTMLElement).id || `option-${index}`, patternType: 'hidden_options', severity: 'critical', description: 'Privacy-protective option is visually hidden or minimized', regulation: 'CPRA §1798.185(a)(4)(C)', remediation: 'Make all options equally visible' }); } } }); } private checkPreselection(container: HTMLElement): void { const checkboxes = container.querySelectorAll('input[type="checkbox"]'); const radios = container.querySelectorAll('input[type="radio"]'); checkboxes.forEach((checkbox, index) => { const input = checkbox as HTMLInputElement; const label = input.labels?.[0]?.textContent || ''; // Check if tracking/marketing options are pre-checked const trackingKeywords = ['marketing', 'advertising', 'personalization', 'tracking', 'analytics', 'third party']; const isTrackingOption = trackingKeywords.some(keyword => label.toLowerCase().includes(keyword)); if (input.checked && isTrackingOption) { this.issues.push({ elementId: input.id || `checkbox-${index}`, patternType: 'preselection', severity: 'high', description: `Tracking option "${label}" is pre-selected`, regulation: 'CPRA §1798.185(a)(4)(B)', remediation: 'Do not pre-select any non-essential data collection options' }); } }); } private checkFalseHierarchy(container: HTMLElement): void { const buttons = Array.from(container.querySelectorAll('button, a[role="button"], [role="button"]')); // Check if accept button appears before reject const acceptIndex = buttons.findIndex(b => (b.textContent?.toLowerCase() || '').includes('accept') || (b.textContent?.toLowerCase() || '').includes('agree') ); const rejectIndex = buttons.findIndex(b => (b.textContent?.toLowerCase() || '').includes('reject') || (b.textContent?.toLowerCase() || '').includes('decline') ); // Check for excessive steps to reject const settingsButton = container.querySelector('[data-action="settings"], .settings-btn'); if (settingsButton && rejectIndex === -1) { // Count clicks to reach reject // If accept is one click but reject requires multiple, that's problematic this.issues.push({ elementId: 'consent-flow', patternType: 'false_hierarchy', severity: 'medium', description: 'Accept requires fewer clicks than reject (asymmetric flow)', regulation: 'CPRA §1798.185(a)(4)(C)', remediation: 'Ensure accepting and rejecting require the same number of clicks' }); } } private checkObstruction(container: HTMLElement): void { // Check for countdown timers const hasCountdown = container.querySelector('[class*="countdown"], [class*="timer"]'); if (hasCountdown) { this.issues.push({ elementId: 'countdown-timer', patternType: 'obstruction', severity: 'critical', description: 'Countdown timer pressures users to make quick decisions', regulation: 'CPRA §1798.185(a)(4)(A)', remediation: 'Remove any time pressure elements' }); } // Check for excessive modal layers const modalCount = document.querySelectorAll('[role="dialog"], .modal').length; if (modalCount > 1) { this.issues.push({ elementId: 'modal-layers', patternType: 'obstruction', severity: 'medium', description: 'Multiple modal dialogs create obstruction', regulation: 'CPRA §1798.185(a)(4)(C)', remediation: 'Use a single, clear consent interface' }); } } private checkTrickWording(container: HTMLElement): void { const trickPatterns = [ { pattern: /double negative/i, description: 'Double negative confuses meaning' }, { pattern: /don'?t\s+not/i, description: 'Double negative confuses meaning' }, { pattern: /uncheck\s+to\s+(not\s+)?share/i, description: 'Reversed checkbox logic' }, { pattern: /leave\s+(un)?checked\s+to/i, description: 'Ambiguous checkbox instruction' } ]; const textContent = container.textContent || ''; for (const { pattern, description } of trickPatterns) { if (pattern.test(textContent)) { this.issues.push({ elementId: 'consent-text', patternType: 'trick_wording', severity: 'high', description, regulation: 'CPRA §1798.185(a)(4)(A)', remediation: 'Use clear, affirmative language' }); } } } private isHighContrast(color: string): boolean { // Simple check - in production, use proper contrast calculation const rgb = color.match(/\d+/g); if (!rgb || rgb.length < 3) return false; const [r, g, b] = rgb.map(Number); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance < 0.5; // Dark colors on light background are high contrast } private calculateComplianceScore(): number { const severityWeights = { critical: 30, high: 20, medium: 10, low: 5 }; const totalDeductions = this.issues.reduce( (sum, issue) => sum + severityWeights[issue.severity], 0 ); return Math.max(0, 100 - totalDeductions); } private generateRecommendations(): string[] { const recommendations: string[] = []; if (this.issues.some(i => i.patternType === 'visual_asymmetry')) { recommendations.push('Ensure all consent options have equal visual weight and prominence'); } if (this.issues.some(i => i.patternType === 'confirm_shaming')) { recommendations.push('Replace manipulative button text with neutral alternatives'); } if (this.issues.some(i => i.patternType === 'preselection')) { recommendations.push('Set all non-essential options to unchecked by default'); } if (this.issues.some(i => i.patternType === 'hidden_options')) { recommendations.push('Display reject option prominently on the initial consent screen'); } if (this.issues.some(i => i.patternType === 'false_hierarchy')) { recommendations.push('Ensure accepting and rejecting require equal effort'); } return recommendations; } } // Usage example const detector = new DarkPatternDetector(); const consentBanner = document.getElementById('consent-banner'); if (consentBanner) { const analysis = detector.analyzeConsentUI(consentBanner); console.log('Consent UI Analysis:', analysis); } ``` ### 3. Inadequate Privacy Notices Privacy notice deficiencies continue to be a common enforcement target. The CPRA requires specific disclosures that many businesses fail to provide. ```typescript // privacy-notice-validator.ts interface PrivacyNoticeRequirements { section: string; requirement: string; cpraReference: string; required: boolean; present: boolean; compliant: boolean; notes: string; } interface PrivacyNoticeAudit { overallCompliant: boolean; score: number; requirements: PrivacyNoticeRequirements[]; missingElements: string[]; recommendations: string[]; } class PrivacyNoticeValidator { // Required elements per CPRA private requirements: Omit[] = [ { section: 'Categories of PI Collected', requirement: 'List categories of personal information collected in the preceding 12 months', cpraReference: '§1798.100(a)(1)', required: true }, { section: 'Categories of Sources', requirement: 'Identify categories of sources from which PI is collected', cpraReference: '§1798.100(a)(2)', required: true }, { section: 'Business Purpose', requirement: 'Describe business or commercial purpose for collecting/selling PI', cpraReference: '§1798.100(a)(3)', required: true }, { section: 'Categories of Third Parties', requirement: 'List categories of third parties to whom PI is disclosed', cpraReference: '§1798.100(a)(4)', required: true }, { section: 'Categories Sold/Shared', requirement: 'Identify categories of PI sold or shared in preceding 12 months', cpraReference: '§1798.100(a)(5)', required: true }, { section: 'Categories per Purpose', requirement: 'For each category of PI, identify business purpose for collection', cpraReference: '§1798.100(a)(6)', required: true }, { section: 'Sensitive PI Disclosure', requirement: 'If applicable, disclosure about sensitive personal information collection', cpraReference: '§1798.121', required: true }, { section: 'Retention Periods', requirement: 'Retention period or criteria for determining retention period per category', cpraReference: '§1798.100(a)(3)', required: true }, { section: 'Consumer Rights', requirement: 'Description of consumer rights (know, delete, correct, opt-out, non-discrimination)', cpraReference: '§1798.100(a)', required: true }, { section: 'Right to Know', requirement: 'Right to know what PI is collected', cpraReference: '§1798.100', required: true }, { section: 'Right to Delete', requirement: 'Right to delete PI', cpraReference: '§1798.105', required: true }, { section: 'Right to Correct', requirement: 'Right to correct inaccurate PI', cpraReference: '§1798.106', required: true }, { section: 'Right to Opt-Out Sale/Share', requirement: 'Right to opt out of sale/sharing', cpraReference: '§1798.120', required: true }, { section: 'Right to Limit SPI', requirement: 'Right to limit use of sensitive personal information', cpraReference: '§1798.121', required: true }, { section: 'Non-Discrimination', requirement: 'Right to non-discrimination for exercising rights', cpraReference: '§1798.125', required: true }, { section: 'Authorized Agent', requirement: 'Information about submitting requests through authorized agents', cpraReference: '§1798.185(a)(7)', required: true }, { section: 'Request Methods', requirement: 'At least two designated methods for submitting requests', cpraReference: '§1798.130(a)(1)', required: true }, { section: 'Response Timeframe', requirement: 'Disclosure that requests will be responded to within 45 days', cpraReference: '§1798.130(a)(2)', required: true }, { section: 'Financial Incentives', requirement: 'Description of financial incentive practices, if any', cpraReference: '§1798.125(b)', required: false }, { section: 'Do Not Sell Link', requirement: '"Do Not Sell or Share My Personal Information" link', cpraReference: '§1798.135(a)(1)', required: true }, { section: 'Limit Sensitive PI Link', requirement: '"Limit the Use of My Sensitive Personal Information" link (if applicable)', cpraReference: '§1798.135(a)(2)', required: false }, { section: 'Contact Information', requirement: 'Contact information for privacy inquiries', cpraReference: '§1798.130(a)(1)', required: true }, { section: 'Last Updated Date', requirement: 'Date of last update to privacy notice', cpraReference: '§1798.100', required: true }, { section: 'Annual Update', requirement: 'Notice updated at least annually', cpraReference: '§1798.100(b)', required: true }, { section: 'Automated Decision-Making', requirement: 'Disclosure about automated decision-making (if applicable)', cpraReference: '§1798.185(a)(16)', required: false } ]; analyzePrivacyNotice(noticeText: string, noticeUrl: string): PrivacyNoticeAudit { const results: PrivacyNoticeRequirements[] = []; const missingElements: string[] = []; for (const req of this.requirements) { const check = this.checkRequirement(req, noticeText); results.push(check); if (req.required && !check.compliant) { missingElements.push(req.section); } } // Additional structural checks const structuralChecks = this.performStructuralChecks(noticeText, noticeUrl); results.push(...structuralChecks); const requiredCount = results.filter(r => r.required).length; const compliantRequired = results.filter(r => r.required && r.compliant).length; const score = Math.round((compliantRequired / requiredCount) * 100); return { overallCompliant: score >= 90, score, requirements: results, missingElements, recommendations: this.generateRecommendations(results) }; } private checkRequirement( req: Omit, text: string ): PrivacyNoticeRequirements { const patterns = this.getPatterns(req.section); const present = patterns.some(pattern => pattern.test(text)); // More sophisticated compliance check let compliant = present; let notes = ''; if (present) { const qualityCheck = this.checkQuality(req.section, text); compliant = qualityCheck.compliant; notes = qualityCheck.notes; } else { notes = `Section "${req.section}" not found in privacy notice`; } return { ...req, present, compliant, notes }; } private getPatterns(section: string): RegExp[] { const patternMap: Record = { 'Categories of PI Collected': [ /categories\s+of\s+personal\s+information/i, /what\s+(personal\s+)?information\s+we\s+collect/i, /types\s+of\s+data\s+collected/i ], 'Categories of Sources': [ /sources?\s+(of|from which)\s+(personal\s+)?information/i, /where\s+we\s+collect/i, /how\s+we\s+collect/i ], 'Business Purpose': [ /business\s+(or\s+commercial\s+)?purpose/i, /why\s+we\s+(collect|use)/i, /purposes?\s+for\s+(collecting|processing)/i ], 'Categories of Third Parties': [ /third\s+parties?\s+(to\s+whom|we\s+(share|disclose))/i, /who\s+we\s+share\s+with/i, /categories?\s+of\s+recipients/i ], 'Categories Sold/Shared': [ /categor(y|ies)\s+of\s+personal\s+information\s+(sold|shared)/i, /do\s+(not\s+)?sell/i, /sale\s+of\s+personal\s+information/i ], 'Retention Periods': [ /retention\s+period/i, /how\s+long\s+we\s+(keep|retain|store)/i, /data\s+retention/i ], 'Right to Know': [ /right\s+to\s+know/i, /right\s+to\s+access/i, /request\s+(to\s+)?know/i ], 'Right to Delete': [ /right\s+to\s+delet(e|ion)/i, /request\s+delet(e|ion)/i, /erasure/i ], 'Right to Correct': [ /right\s+to\s+correct/i, /rectification/i, /update\s+(your|personal)\s+(information|data)/i ], 'Right to Opt-Out Sale/Share': [ /opt[\s-]?out\s+(of\s+)?(the\s+)?(sale|sharing)/i, /do\s+not\s+sell/i, /right\s+to\s+opt[\s-]?out/i ], 'Do Not Sell Link': [ /do\s+not\s+sell\s+(or\s+share\s+)?my\s+personal\s+information/i, /opt[\s-]?out\s+(of\s+)?sale/i ], 'Contact Information': [ /contact\s+us/i, /privacy\s+(officer|contact|inquiries)/i, /email.*privacy/i, /privacy@/i ], 'Last Updated Date': [ /last\s+(updated|modified|revised)/i, /effective\s+date/i, /as\s+of\s+\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/i ] }; return patternMap[section] || [new RegExp(section.replace(/\s+/g, '\\s+'), 'i')]; } private checkQuality(section: string, text: string): { compliant: boolean; notes: string } { // Section-specific quality checks switch (section) { case 'Categories of PI Collected': // Should list specific categories, not just mention the concept const hasCategories = /identifiers|commercial\s+information|internet\s+activity|geolocation|biometric|audio|visual|professional|education|inferences/i.test(text); return { compliant: hasCategories, notes: hasCategories ? 'Lists specific categories' : 'Should enumerate specific categories of PI' }; case 'Retention Periods': // Should include actual timeframes const hasTimeframes = /\d+\s*(days?|months?|years?)|as\s+long\s+as\s+necessary|indefinitely|until\s+you\s+request/i.test(text); return { compliant: hasTimeframes, notes: hasTimeframes ? 'Includes retention timeframes' : 'Should specify actual retention periods or criteria' }; case 'Request Methods': // Should have at least two methods const methods = [ /email/i.test(text), /web\s*form|online\s*form|submit\s*a\s*request/i.test(text), /toll[\s-]?free|1[\s-]?8\d{2}/i.test(text), /mail|postal/i.test(text) ].filter(Boolean).length; return { compliant: methods >= 2, notes: methods >= 2 ? `${methods} request methods found` : 'Must provide at least two methods for submitting requests' }; default: return { compliant: true, notes: 'Section present' }; } } private performStructuralChecks(text: string, url: string): PrivacyNoticeRequirements[] { const checks: PrivacyNoticeRequirements[] = []; // Check readability const wordCount = text.split(/\s+/).length; const sentenceCount = text.split(/[.!?]+/).length; const avgWordsPerSentence = wordCount / sentenceCount; checks.push({ section: 'Readability', requirement: 'Notice should be written in plain language', cpraReference: '§1798.185(a)(5)', required: false, present: true, compliant: avgWordsPerSentence < 25, notes: avgWordsPerSentence < 25 ? 'Readable sentence length' : `Average ${Math.round(avgWordsPerSentence)} words per sentence - consider simplifying` }); // Check if accessible (basic check) checks.push({ section: 'Accessibility', requirement: 'Notice must be accessible', cpraReference: '§1798.185(a)(5)', required: true, present: true, compliant: !url.includes('.pdf'), // PDFs are harder to make accessible notes: url.includes('.pdf') ? 'Consider HTML version for better accessibility' : 'HTML format supports accessibility' }); return checks; } private generateRecommendations(results: PrivacyNoticeRequirements[]): string[] { const recommendations: string[] = []; const missing = results.filter(r => r.required && !r.compliant); if (missing.length > 0) { recommendations.push(`Add or update the following required sections: ${missing.map(m => m.section).join(', ')}`); } // Specific recommendations const retentionCheck = results.find(r => r.section === 'Retention Periods'); if (retentionCheck && !retentionCheck.compliant) { recommendations.push('Specify concrete retention periods (e.g., "3 years" or "duration of business relationship plus 2 years")'); } const requestMethods = results.find(r => r.section === 'Request Methods'); if (requestMethods && !requestMethods.compliant) { recommendations.push('Provide at least two methods for submitting requests (e.g., web form, email, toll-free number)'); } const doNotSell = results.find(r => r.section === 'Do Not Sell Link'); if (doNotSell && !doNotSell.present) { recommendations.push('Add clear "Do Not Sell or Share My Personal Information" link (required if you sell/share data)'); } return recommendations; } } // Generate a privacy notice template function generateCompliantPrivacyNotice(businessInfo: { companyName: string; effectiveDate: string; piCategories: string[]; purposes: string[]; thirdParties: string[]; retentionPeriods: Record; contactEmail: string; requestUrl: string; }): string { return ` # Privacy Notice for California Residents **Effective Date:** ${businessInfo.effectiveDate} **Last Updated:** ${businessInfo.effectiveDate} This Privacy Notice for California Residents supplements the information contained in ${businessInfo.companyName}'s Privacy Policy and applies solely to visitors, users, and others who reside in the State of California ("consumers" or "you"). ## Categories of Personal Information We Collect In the preceding 12 months, we have collected the following categories of personal information: ${businessInfo.piCategories.map((cat, i) => `${i + 1}. **${cat}**`).join('\n')} ## Sources of Personal Information We collect personal information from the following sources: - Directly from you when you provide it to us - Automatically when you use our services - From third-party partners and service providers - From publicly available sources ## Purposes for Collection We collect and use personal information for the following business and commercial purposes: ${businessInfo.purposes.map((purpose, i) => `${i + 1}. ${purpose}`).join('\n')} ## Categories of Third Parties We may disclose your personal information to the following categories of third parties: ${businessInfo.thirdParties.map((party, i) => `- ${party}`).join('\n')} ## Sale and Sharing of Personal Information ${businessInfo.companyName} [does/does not] "sell" personal information as defined by the CCPA/CPRA. ${businessInfo.companyName} [does/does not] "share" personal information for cross-context behavioral advertising. If you would like to opt out of the sale or sharing of your personal information, please click the following link: **[Do Not Sell or Share My Personal Information](${businessInfo.requestUrl})** ## Sensitive Personal Information We [do/do not] collect sensitive personal information. [If yes, describe categories and purposes] To limit our use of your sensitive personal information, please click: **[Limit the Use of My Sensitive Personal Information](${businessInfo.requestUrl})** ## Data Retention We retain personal information for the following periods: ${Object.entries(businessInfo.retentionPeriods).map(([cat, period]) => `- **${cat}:** ${period}`).join('\n')} ## Your California Privacy Rights As a California resident, you have the following rights: ### Right to Know You have the right to request information about: - Categories of personal information we collected - Categories of sources from which we collected it - Business or commercial purposes for collecting, selling, or sharing - Categories of third parties to whom we disclosed it - Specific pieces of personal information we collected about you ### Right to Delete You have the right to request deletion of your personal information, subject to certain exceptions. ### Right to Correct You have the right to request correction of inaccurate personal information. ### Right to Opt-Out of Sale/Sharing You have the right to opt out of the sale or sharing of your personal information. ### Right to Limit Use of Sensitive PI You have the right to limit our use and disclosure of sensitive personal information. ### Right to Non-Discrimination We will not discriminate against you for exercising any of your privacy rights. ## Submitting Requests You may submit privacy requests through the following methods: 1. **Online Form:** [${businessInfo.requestUrl}](${businessInfo.requestUrl}) 2. **Email:** ${businessInfo.contactEmail} We will respond to verifiable consumer requests within 45 days. If we require more time (up to 90 days), we will inform you of the reason. ## Authorized Agents You may designate an authorized agent to submit requests on your behalf. To do so, provide the agent with written permission and verify your identity directly with us. ## Contact Us For questions about this Privacy Notice or our privacy practices: **Email:** ${businessInfo.contactEmail} ## Changes to This Notice We will update this Privacy Notice annually, or more frequently as required. We will post the updated notice on our website with a new effective date. `; } ``` ### 4. Service Provider and Contractor Compliance The CPRA introduced strict requirements for service provider and contractor relationships. ```typescript // service-provider-compliance.ts interface ServiceProviderRequirements { contractRequired: boolean; requiredClauses: ContractClause[]; prohibitedActivities: string[]; auditRights: boolean; certificationRequired: boolean; } interface ContractClause { id: string; title: string; requirement: string; cpraReference: string; sampleLanguage: string; required: boolean; } interface VendorComplianceAssessment { vendorName: string; relationship: 'service_provider' | 'contractor' | 'third_party'; contractCompliant: boolean; missingClauses: string[]; riskLevel: 'low' | 'medium' | 'high' | 'critical'; recommendations: string[]; } class ServiceProviderComplianceManager { private requiredClauses: ContractClause[] = [ { id: 'purpose_limitation', title: 'Purpose Limitation', requirement: 'Prohibit processing for purposes other than those specified in the contract', cpraReference: '§1798.140(ag)(1)(A)', sampleLanguage: 'Service Provider shall process Personal Information solely for the specific business purposes set forth in this Agreement and shall not process Personal Information for any other purpose.', required: true }, { id: 'prohibition_selling', title: 'Prohibition on Selling/Sharing', requirement: 'Prohibit selling or sharing personal information', cpraReference: '§1798.140(ag)(1)(B)', sampleLanguage: 'Service Provider shall not sell or share Personal Information received from Business. "Sell" and "share" have the meanings defined in the CCPA/CPRA.', required: true }, { id: 'retention_deletion', title: 'Retention and Deletion', requirement: 'Require retention only as necessary and deletion upon request', cpraReference: '§1798.140(ag)(1)(C)', sampleLanguage: 'Service Provider shall retain Personal Information only for the period necessary to fulfill the business purposes specified herein, and shall delete or return all Personal Information upon written request or termination of this Agreement.', required: true }, { id: 'compliance_notification', title: 'Compliance Notification', requirement: 'Require notification of inability to comply', cpraReference: '§1798.140(ag)(1)(D)', sampleLanguage: 'Service Provider shall notify Business if it determines that it can no longer meet its obligations under applicable privacy laws, and Business shall have the right to take reasonable and appropriate steps to stop and remediate unauthorized use.', required: true }, { id: 'audit_rights', title: 'Audit and Assessment Rights', requirement: 'Grant audit rights to ensure compliance', cpraReference: '§1798.140(ag)(1)(E)', sampleLanguage: 'Business shall have the right, upon reasonable notice, to audit or assess Service Provider\'s compliance with this Agreement and applicable privacy laws, including through manual reviews, automated scans, regular assessments, audits, or other technical and operational testing.', required: true }, { id: 'subcontractor_requirements', title: 'Subcontractor Flow-Down', requirement: 'Require same obligations for subcontractors', cpraReference: '§1798.140(ag)(1)(F)', sampleLanguage: 'Service Provider shall not engage any subcontractor to process Personal Information without Business\'s prior written consent. Any approved subcontractor shall be bound by written terms at least as protective as those in this Agreement.', required: true }, { id: 'combination_prohibition', title: 'Combination Prohibition', requirement: 'Prohibit combining personal information from multiple sources', cpraReference: '§1798.140(ag)(1)(G)', sampleLanguage: 'Service Provider shall not combine Personal Information received from Business with Personal Information received from other sources or collected from its own interactions with the consumer, except as expressly permitted by the CCPA/CPRA.', required: true }, { id: 'certification', title: 'Compliance Certification', requirement: 'Certify understanding and compliance with restrictions', cpraReference: '§1798.140(ag)(2)', sampleLanguage: 'Service Provider certifies that it understands and will comply with the restrictions set forth in this Agreement and applicable privacy laws, and will not take any action that would cause Business to violate the CCPA/CPRA.', required: true }, { id: 'consumer_request_assistance', title: 'Consumer Request Assistance', requirement: 'Assist with consumer rights requests', cpraReference: '§1798.105(c)', sampleLanguage: 'Service Provider shall assist Business in responding to verifiable consumer requests, including requests to know, delete, correct, and opt-out, within the timeframes required by applicable law.', required: true }, { id: 'security_measures', title: 'Security Requirements', requirement: 'Implement appropriate security measures', cpraReference: '§1798.81.5', sampleLanguage: 'Service Provider shall implement and maintain reasonable security measures appropriate to the nature of the Personal Information processed, including encryption, access controls, and regular security assessments.', required: true } ]; assessVendorContract( vendorName: string, contractText: string, dataCategories: string[] ): VendorComplianceAssessment { const missingClauses: string[] = []; let riskScore = 0; for (const clause of this.requiredClauses) { if (!this.clausePresent(clause, contractText)) { missingClauses.push(clause.title); riskScore += clause.required ? 10 : 5; } } // Assess relationship type based on contract const relationship = this.determineRelationship(contractText); // Additional risk factors if (dataCategories.some(cat => ['ssn', 'financial', 'health', 'biometric', 'precise_geolocation'].includes(cat.toLowerCase()) )) { riskScore += 20; // Sensitive data increases risk } const riskLevel = riskScore >= 50 ? 'critical' : riskScore >= 30 ? 'high' : riskScore >= 15 ? 'medium' : 'low'; return { vendorName, relationship, contractCompliant: missingClauses.length === 0, missingClauses, riskLevel, recommendations: this.generateVendorRecommendations(missingClauses, relationship) }; } private clausePresent(clause: ContractClause, contractText: string): boolean { // Simplified pattern matching - in production, use more sophisticated NLP const patterns: Record = { purpose_limitation: [ /specific\s+(business\s+)?purpose/i, /solely\s+for\s+the\s+purpose/i, /limited\s+to\s+(the\s+)?purpose/i ], prohibition_selling: [ /shall\s+not\s+sell/i, /prohibited\s+from\s+selling/i, /no\s+sale\s+of/i ], retention_deletion: [ /delete\s+(or\s+return)?\s+.*upon/i, /retention\s+(period|only)/i, /return\s+or\s+destroy/i ], compliance_notification: [ /notify.*unable\s+to\s+comply/i, /notification.*non-?compliance/i, /inform.*cannot\s+meet/i ], audit_rights: [ /right\s+to\s+audit/i, /audit\s+(or\s+)?assess/i, /inspection\s+rights/i ], subcontractor_requirements: [ /subcontractor.*same\s+obligations/i, /flow[\s-]?down/i, /sub-?process.*written\s+consent/i ], combination_prohibition: [ /shall\s+not\s+combine/i, /prohibited.*combin/i, /no\s+combination/i ], certification: [ /certif(y|ies|ication)/i, /warrant(s|y)/i, /represent(s|ation)/i ], consumer_request_assistance: [ /assist.*consumer\s+request/i, /support.*data\s+subject/i, /cooperate.*privacy\s+request/i ], security_measures: [ /reasonable\s+security/i, /appropriate\s+security\s+measures/i, /implement.*safeguards/i ] }; const clausePatterns = patterns[clause.id] || []; return clausePatterns.some(pattern => pattern.test(contractText)); } private determineRelationship(contractText: string): 'service_provider' | 'contractor' | 'third_party' { // Service provider: Processes on behalf of business if (/on\s+behalf\s+of|process.*for\s+the\s+business|data\s+processor/i.test(contractText)) { return 'service_provider'; } // Contractor: Disclosed for business purpose with certain restrictions if (/contractor|independent\s+contractor|disclosed\s+for.*business\s+purpose/i.test(contractText)) { return 'contractor'; } // Third party: Can use data for their own purposes return 'third_party'; } private generateVendorRecommendations( missingClauses: string[], relationship: string ): string[] { const recommendations: string[] = []; if (missingClauses.length > 0) { recommendations.push( `Execute DPA addendum adding: ${missingClauses.join(', ')}` ); } if (relationship === 'third_party') { recommendations.push( 'Third-party relationship requires consumer opt-out rights - add "Do Not Sell/Share" disclosure' ); recommendations.push( 'Consider restructuring as service provider relationship for reduced disclosure requirements' ); } if (missingClauses.includes('Audit and Assessment Rights')) { recommendations.push( 'Without audit rights, you cannot verify compliance - this is a critical gap' ); } return recommendations; } // Generate compliant DPA template generateDPATemplate(businessName: string, vendorName: string): string { const clauses = this.requiredClauses.map(clause => ` ## ${clause.title} ${clause.sampleLanguage} *(CCPA/CPRA Reference: ${clause.cpraReference})* `).join('\n'); return ` # DATA PROCESSING ADDENDUM This Data Processing Addendum ("DPA") is entered into by and between: **"Business"**: ${businessName} **"Service Provider"**: ${vendorName} This DPA supplements and is incorporated into the underlying agreement between the parties (the "Agreement") and governs the processing of Personal Information (as defined in the California Consumer Privacy Act of 2018, as amended by the California Privacy Rights Act of 2020, collectively "CCPA/CPRA"). --- ${clauses} --- ## Execution This DPA is effective as of the date last signed below. **${businessName}** Signature: _______________________ Name: _______________________ Title: _______________________ Date: _______________________ **${vendorName}** Signature: _______________________ Name: _______________________ Title: _______________________ Date: _______________________ `; } } ``` ## Lessons Learned from Enforcement ### Key Takeaways from CPPA Actions | Lesson | Enforcement Example | Implementation | |--------|---------------------|----------------| | **GPC is Mandatory** | Beauty retailer fined $1.2M | Treat GPC as valid opt-out in California | | **Transparency is Key** | Multiple notices for inadequate disclosure | Regular privacy notice audits | | **Easy Opt-Out** | Dark pattern enforcement | Symmetric design for consent choices | | **Vendor Vetting** | Health app settlement | DPA requirements for all vendors | | **Regular Audits** | Ongoing enforcement | Continuous compliance monitoring | ### Compliance Implementation Checklist ```typescript // ccpa-compliance-checklist.ts interface ComplianceChecklistItem { id: string; category: string; requirement: string; status: 'compliant' | 'partial' | 'non_compliant' | 'not_applicable'; priority: 'critical' | 'high' | 'medium' | 'low'; deadline: string; owner: string; evidence: string[]; notes: string; } const ccpaComplianceChecklist: ComplianceChecklistItem[] = [ // GPC Compliance { id: 'gpc-001', category: 'Opt-Out Rights', requirement: 'Detect and honor Global Privacy Control signals', status: 'non_compliant', priority: 'critical', deadline: '30 days', owner: 'Engineering', evidence: [], notes: 'Implement GPC detection in consent manager' }, { id: 'gpc-002', category: 'Opt-Out Rights', requirement: 'Block sale/sharing when GPC detected', status: 'non_compliant', priority: 'critical', deadline: '30 days', owner: 'Engineering', evidence: [], notes: 'Update tag firing rules' }, // Do Not Sell/Share Link { id: 'dns-001', category: 'Consumer Rights', requirement: '"Do Not Sell or Share My Personal Information" link present', status: 'non_compliant', priority: 'critical', deadline: '7 days', owner: 'Web Team', evidence: [], notes: 'Add to footer on all pages' }, { id: 'dns-002', category: 'Consumer Rights', requirement: 'Link leads to functional opt-out mechanism', status: 'non_compliant', priority: 'critical', deadline: '14 days', owner: 'Engineering', evidence: [], notes: 'Build opt-out preference center' }, // Privacy Notice { id: 'pn-001', category: 'Disclosures', requirement: 'Privacy notice includes all CPRA-required disclosures', status: 'partial', priority: 'high', deadline: '30 days', owner: 'Legal', evidence: [], notes: 'Add retention periods and SPI disclosures' }, { id: 'pn-002', category: 'Disclosures', requirement: 'Privacy notice updated in last 12 months', status: 'compliant', priority: 'medium', deadline: 'N/A', owner: 'Legal', evidence: ['Privacy policy dated January 2025'], notes: '' }, // Consumer Request Handling { id: 'dsar-001', category: 'Consumer Rights', requirement: 'At least two methods for submitting requests', status: 'compliant', priority: 'high', deadline: 'N/A', owner: 'Customer Service', evidence: ['Web form', 'Email address'], notes: '' }, { id: 'dsar-002', category: 'Consumer Rights', requirement: '45-day response timeline met', status: 'partial', priority: 'high', deadline: '60 days', owner: 'Customer Service', evidence: [], notes: 'Automate intake and tracking' }, // Service Provider Contracts { id: 'sp-001', category: 'Vendor Management', requirement: 'DPAs executed with all service providers', status: 'partial', priority: 'high', deadline: '90 days', owner: 'Procurement', evidence: [], notes: '15 vendors need updated agreements' }, { id: 'sp-002', category: 'Vendor Management', requirement: 'Service provider contracts include all required clauses', status: 'partial', priority: 'high', deadline: '90 days', owner: 'Legal', evidence: [], notes: 'Audit all existing DPAs' }, // Consent UI { id: 'cui-001', category: 'Dark Patterns', requirement: 'Consent UI free of dark patterns', status: 'non_compliant', priority: 'critical', deadline: '30 days', owner: 'UX/Engineering', evidence: [], notes: 'Redesign cookie banner per CPRA requirements' }, { id: 'cui-002', category: 'Dark Patterns', requirement: 'Accept and reject options equally prominent', status: 'non_compliant', priority: 'critical', deadline: '30 days', owner: 'UX/Engineering', evidence: [], notes: 'Update button styling' }, // Minors { id: 'min-001', category: 'Minors Protection', requirement: 'Opt-in consent for selling data of consumers under 16', status: 'not_applicable', priority: 'high', deadline: 'N/A', owner: 'Product', evidence: [], notes: 'Product not targeted at minors' }, // Security { id: 'sec-001', category: 'Security', requirement: 'Reasonable security measures implemented', status: 'compliant', priority: 'critical', deadline: 'N/A', owner: 'Security', evidence: ['SOC 2 Type II', 'Penetration test report'], notes: '' } ]; // Calculate compliance score function calculateComplianceScore(checklist: ComplianceChecklistItem[]): number { const weights = { critical: 4, high: 3, medium: 2, low: 1 }; let totalWeight = 0; let compliantWeight = 0; for (const item of checklist) { if (item.status === 'not_applicable') continue; const weight = weights[item.priority]; totalWeight += weight; if (item.status === 'compliant') { compliantWeight += weight; } else if (item.status === 'partial') { compliantWeight += weight * 0.5; } } return Math.round((compliantWeight / totalWeight) * 100); } console.log('Compliance Score:', calculateComplianceScore(ccpaComplianceChecklist)); ``` ## What enforcement means for your roadmap The California Privacy Protection Agency (CPPA) and the Attorney General are both actively enforcing the CPRA. The 30-day cure period is gone, and public examples—including Sephora in 2022 and multiple CPPA actions in 2024—show that failing to honor opt-outs or GPC signals quickly leads to penalties and mandated remediation plans. Move beyond checkbox compliance and embed privacy-by-design to avoid being the next headline. ### Key Enforcement Trends to Watch 1. **GPC enforcement will intensify** - The CPPA has made clear that failing to honor GPC is a priority violation 2. **Dark patterns face increased scrutiny** - Expect more detailed guidance and enforcement on UI design 3. **Automated decision-making regulations coming** - Prepare for transparency requirements around AI/ML 4. **Cross-regulatory coordination increasing** - CPPA working with other state AGs and international regulators 5. **Minors' data protection expanding** - California Age-Appropriate Design Code adds new requirements ### Your Compliance Action Plan 1. **Immediate (0-30 days)** - Implement GPC signal detection and honoring - Add "Do Not Sell or Share" link to all pages - Audit consent UI for dark patterns 2. **Short-term (30-90 days)** - Update privacy notice with all required disclosures - Execute DPAs with all service providers - Implement automated DSAR handling 3. **Ongoing** - Quarterly compliance audits - Annual privacy notice updates - Continuous monitoring of regulatory guidance The cost of non-compliance—both in fines and reputational damage—far exceeds the investment in proper privacy infrastructure. Start today, and build privacy into your business operations, not as an afterthought, but as a competitive advantage.
R

Rachel Torres, Privacy Counsel

Escritor no GetCookies, especializado em conformidade de privacidade, gestão de consentimento e otimização de marketing digital.

Pronto para simplificar o consentimento de cookies?

O GetCookies torna a conformidade com RGPD, CCPA e privacidade global fácil. Comece hoje.