Takaisin blogiin
Compliance

CCPA to CPRA: The Complete California Privacy Compliance Update for 2025

Rachel Torres, Privacy CounselNovember 24, 202517 min lukuaika
CCPACPRACaliforniaGPCCompliance

TLDR: CPRA enforcement is fully active. In 2025, honoring Global Privacy Control (GPC) signals is mandatory, not optional. Sephora's $1.2M fine was just the beginning.

Read full summary The California Privacy Rights Act significantly expanded CCPA obligations. Key 2025 requirements include mandatory GPC signal recognition, new sensitive personal information assessments, and strict "dark pattern" prohibitions. This guide provides complete implementation code for GPC detection, sensitive PI handling, and compliance automation. *Summary by Claude AI*
## The $1.2 Million Mistake In August 2022, Sephora paid California $1.2 million for one oversight: their website didn't respect Global Privacy Control signals. Their banner looked compliant. Users could "opt out" on a page buried three clicks deep. But when a California user's browser sent a GPC signal—an automatic "do not sell my data" request—Sephora ignored it. They kept sharing customer data with advertising networks as if nothing happened. The California Attorney General made an example of them. And that was under the old rules. In 2025, enforcement is worse. The California Privacy Protection Agency (CPPA) has full authority, larger budgets, and a mandate to crack down on exactly the behavior that got Sephora fined. The difference? Now there's no cure period for many violations. No warning letter. Just an investigation and a penalty. If your website ignores GPC signals today, you're not taking a calculated risk. You're hoping not to get caught. ## What Changed in 2025 The California privacy landscape looks nothing like it did when CCPA launched in 2020. The California Privacy Rights Act (CPRA) transformed a basic consumer protection law into something approaching GDPR-level regulation: **Mandatory GPC Recognition**: The CPPA has confirmed that Global Privacy Control signals must be treated as valid opt-out requests. No exceptions. No "we'll consider it." When a browser sends GPC, you stop selling that user's data. Period. **Dark Pattern Crackdown**: Those clever consent banners with giant "Accept All" buttons and tiny "Reject" links? They're now explicitly illegal. Equal prominence is required. The CPPA has fined companies for asymmetric design. **Sensitive Personal Information**: New category, new obligations. Health data, financial information, precise geolocation—all now require separate consent flows and enhanced protections. **No Cure Period**: For many violations, the 30-day "fix it" window is gone. If the CPPA investigates and finds problems, penalties start immediately. This guide covers everything you need to stay compliant: GPC detection code, dark pattern prevention, sensitive PI handling, and service provider contract requirements. ## CPPA Enforcement Priorities in 2025 The California Privacy Protection Agency has identified four priority enforcement areas, each requiring specific technical implementations. ### Priority 1: Dark Patterns in Opt-Out Mechanisms The CPPA has made clear that dark patterns in privacy interfaces will result in enforcement actions. This includes: - **Asymmetric choice architecture**: Making "Accept All" prominent while hiding "Reject All" - **Confirm-shaming**: Language designed to make users feel guilty for protecting privacy - **Obstruction**: Multi-step processes for opting out vs. single-click for opting in - **Forced action**: Requiring account creation or excessive information to exercise rights ```typescript // cpra-compliant-banner.ts // Banner implementation that avoids dark patterns interface CPRABannerConfig { domain: string; privacyPolicyUrl: string; doNotSellUrl: string; requestDeletionUrl: string; contactEmail: string; } interface BannerButton { text: string; action: () => void; style: 'primary' | 'secondary' | 'tertiary'; prominence: number; // 1-10 scale, must be equal for opt-in/opt-out } class CPRACompliantBanner { private config: CPRABannerConfig; private gpcDetected: boolean = false; private userOptedOut: boolean = false; constructor(config: CPRABannerConfig) { this.config = config; this.detectGPC(); this.checkExistingPreference(); } private detectGPC(): void { // GPC detection must happen before any data collection if (typeof navigator !== 'undefined') { this.gpcDetected = !!(navigator as any).globalPrivacyControl; if (this.gpcDetected) { console.log('[CPRA] GPC signal detected - treating as opt-out'); this.handleGPCOptOut(); } } } private handleGPCOptOut(): void { // Immediate opt-out - no confirmation required this.userOptedOut = true; this.disableDataSaleAndSharing(); this.recordOptOutEvent('gpc_signal'); // Do NOT show banner asking to confirm GPC choice // This would violate CPRA requirements } private checkExistingPreference(): void { const stored = localStorage.getItem('cpra_consent_preference'); if (stored) { const preference = JSON.parse(stored); this.userOptedOut = preference.optedOut; } } // Dark pattern prevention: Equal prominence for all choices generateButtons(): BannerButton[] { const buttons: BannerButton[] = [ { text: 'Accept All', action: () => this.handleAcceptAll(), style: 'primary', prominence: 8 // Same prominence }, { text: 'Reject All', action: () => this.handleRejectAll(), style: 'primary', // Same style - not secondary or tertiary prominence: 8 // Same prominence }, { text: 'Customize', action: () => this.showPreferenceCenter(), style: 'secondary', prominence: 6 } ]; return buttons; } // Validate banner doesn't use dark patterns validateCompliance(): ComplianceReport { const issues: string[] = []; const buttons = this.generateButtons(); const acceptBtn = buttons.find(b => b.text.toLowerCase().includes('accept')); const rejectBtn = buttons.find(b => b.text.toLowerCase().includes('reject')); if (acceptBtn && rejectBtn) { // Check equal prominence if (acceptBtn.prominence !== rejectBtn.prominence) { issues.push('Unequal button prominence detected - potential dark pattern'); } // Check equal styling if (acceptBtn.style !== rejectBtn.style) { issues.push('Unequal button styling detected - potential dark pattern'); } } // Check for confirm-shaming language const shamingPhrases = [ 'no thanks', 'i don\'t care', 'i accept risk', 'not interested in', 'miss out', 'lose access' ]; buttons.forEach(btn => { const lowerText = btn.text.toLowerCase(); shamingPhrases.forEach(phrase => { if (lowerText.includes(phrase)) { issues.push(`Potential confirm-shaming detected: "${btn.text}"`); } }); }); return { compliant: issues.length === 0, issues, checkedAt: new Date().toISOString(), regulation: 'CPRA' }; } private handleAcceptAll(): void { this.recordConsent({ analytics: true, advertising: true, personalization: true, dataSale: true, dataSharing: true }); this.hideBanner(); } private handleRejectAll(): void { this.userOptedOut = true; this.recordConsent({ analytics: false, advertising: false, personalization: false, dataSale: false, dataSharing: false }); this.disableDataSaleAndSharing(); this.hideBanner(); } private disableDataSaleAndSharing(): void { // Immediately stop all sale/sharing activities window.dispatchEvent(new CustomEvent('cpra:optout', { detail: { type: 'sale_and_sharing', timestamp: Date.now() } })); // Disable advertising pixels this.disableAdvertisingPixels(); // Stop cross-context behavioral tracking this.disableCrossContextTracking(); } private disableAdvertisingPixels(): void { // Remove Facebook Pixel if (typeof fbq !== 'undefined') { fbq('consent', 'revoke'); } // Remove Google Ads remarketing if (typeof gtag !== 'undefined') { gtag('consent', 'update', { 'ad_storage': 'denied', 'ad_user_data': 'denied', 'ad_personalization': 'denied' }); } // Remove TikTok Pixel if (typeof ttq !== 'undefined') { // TikTok doesn't have native consent mode // Must remove script entirely const tiktokScript = document.querySelector('script[src*="tiktok"]'); tiktokScript?.remove(); } } private disableCrossContextTracking(): void { // Clear third-party cookies where possible document.cookie.split(';').forEach(cookie => { const name = cookie.split('=')[0].trim(); // Clear known tracking cookies const trackingCookies = ['_fbp', '_fbc', '_gcl_au', '_ga', '_gid']; if (trackingCookies.includes(name)) { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; } }); } private recordConsent(preferences: ConsentPreferences): void { const record = { preferences, timestamp: new Date().toISOString(), gpcSignal: this.gpcDetected, userAgent: navigator.userAgent, optedOut: !preferences.dataSale }; localStorage.setItem('cpra_consent_preference', JSON.stringify(record)); // Send to server for audit trail this.sendConsentToServer(record); } private async sendConsentToServer(record: any): Promise { try { await fetch('/api/v1/consent/cpra', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(record) }); } catch (error) { console.error('[CPRA] Failed to record consent:', error); // Queue for retry this.queueForRetry(record); } } private recordOptOutEvent(source: string): void { const event = { type: 'opt_out', source, timestamp: new Date().toISOString(), gpcDetected: this.gpcDetected }; // Store locally for audit const events = JSON.parse(localStorage.getItem('cpra_events') || '[]'); events.push(event); localStorage.setItem('cpra_events', JSON.stringify(events)); } private showPreferenceCenter(): void { window.dispatchEvent(new CustomEvent('cpra:showPreferences')); } private hideBanner(): void { window.dispatchEvent(new CustomEvent('cpra:hideBanner')); } private queueForRetry(record: any): void { const queue = JSON.parse(localStorage.getItem('cpra_retry_queue') || '[]'); queue.push({ record, attempts: 0, nextRetry: Date.now() + 60000 }); localStorage.setItem('cpra_retry_queue', JSON.stringify(queue)); } } interface ConsentPreferences { analytics: boolean; advertising: boolean; personalization: boolean; dataSale: boolean; dataSharing: boolean; } interface ComplianceReport { compliant: boolean; issues: string[]; checkedAt: string; regulation: string; } // Declare global types for third-party pixels declare const fbq: any; declare const gtag: any; declare const ttq: any; ``` ### Priority 2: Global Privacy Control Recognition (Mandatory) GPC recognition is **mandatory** in 2025. The California Attorney General and CPPA have confirmed that treating the GPC signal as a valid consumer request to opt-out of sale/sharing is required by law. Failure to honor this signal is a primary trigger for enforcement actions. ```typescript // gpc-detection-service.ts // Comprehensive GPC detection and handling interface GPCState { detected: boolean; source: 'navigator' | 'header' | 'extension' | 'unknown'; timestamp: string; honored: boolean; } interface GPCComplianceConfig { honorImmediately: boolean; logAllDetections: boolean; notifyBackend: boolean; applyToAllSessions: boolean; } class GPCDetectionService { private state: GPCState; private config: GPCComplianceConfig; private detectionListeners: Set<(state: GPCState) => void> = new Set(); constructor(config: GPCComplianceConfig) { this.config = config; this.state = { detected: false, source: 'unknown', timestamp: new Date().toISOString(), honored: false }; } // Must be called before ANY data collection async detectGPC(): Promise { // Method 1: Navigator API (most common) const navigatorGPC = this.detectNavigatorGPC(); // Method 2: Check for GPC header (server-side indication) const headerGPC = await this.checkGPCHeader(); // Method 3: Check for known privacy extensions const extensionGPC = this.detectPrivacyExtensions(); // Any positive signal means opt-out const detected = navigatorGPC || headerGPC || extensionGPC; this.state = { detected, source: navigatorGPC ? 'navigator' : headerGPC ? 'header' : extensionGPC ? 'extension' : 'unknown', timestamp: new Date().toISOString(), honored: false }; if (detected && this.config.honorImmediately) { await this.honorGPCSignal(); } if (this.config.logAllDetections) { this.logDetection(); } return this.state; } private detectNavigatorGPC(): boolean { if (typeof navigator === 'undefined') return false; // Standard GPC API const nav = navigator as any; // Check globalPrivacyControl property if (nav.globalPrivacyControl === true) { return true; } // Some browsers use different property names if (nav.doNotTrack === '1' || nav.doNotTrack === 'yes') { // Note: DNT is deprecated but some treat it as GPC equivalent // CPPA guidance suggests honoring DNT as well return true; } return false; } private async checkGPCHeader(): Promise { // Server should pass GPC header status to client // This catches cases where extension sets header but not navigator try { const response = await fetch('/api/v1/privacy/gpc-status', { method: 'HEAD', credentials: 'same-origin' }); return response.headers.get('X-GPC-Detected') === 'true'; } catch { return false; } } private detectPrivacyExtensions(): boolean { // Detect common privacy extensions that set GPC // These may not always set navigator.globalPrivacyControl if (typeof document === 'undefined') return false; // Check for Privacy Badger const privacyBadger = document.documentElement.getAttribute('data-pb-enabled'); if (privacyBadger === 'true') return true; // Check for DuckDuckGo Privacy Essentials const ddg = (window as any).__DDG_EXTENSION__; if (ddg && ddg.gpc) return true; // Check for Brave browser const brave = (navigator as any).brave; if (brave && typeof brave.isBrave === 'function') { // Brave has GPC enabled by default return true; } return false; } async honorGPCSignal(): Promise { if (!this.state.detected) return; console.log('[GPC] Honoring Global Privacy Control signal'); // 1. Disable sale of personal information this.disableDataSale(); // 2. Disable sharing for cross-context behavioral advertising this.disableDataSharing(); // 3. Update consent state this.updateConsentForGPC(); // 4. Notify backend if (this.config.notifyBackend) { await this.notifyBackendOfGPC(); } // 5. Mark as honored this.state.honored = true; // 6. Dispatch event for other components this.dispatchGPCEvent(); // 7. Notify listeners this.notifyListeners(); } private disableDataSale(): void { // Set consent mode to deny sale if (typeof gtag !== 'undefined') { gtag('consent', 'update', { 'ad_storage': 'denied', 'ad_user_data': 'denied', 'ad_personalization': 'denied' }); } // Disable any sale-related processing window.dispatchEvent(new CustomEvent('privacy:disableSale')); } private disableDataSharing(): void { // Stop cross-context behavioral advertising const sharingCategories = [ 'advertising', 'cross_site_tracking', 'remarketing', 'audience_building' ]; sharingCategories.forEach(category => { window.dispatchEvent(new CustomEvent('privacy:disableCategory', { detail: { category } })); }); } private updateConsentForGPC(): void { const gpcConsent = { necessary: true, // Always allowed functional: true, // Usually allowed analytics: true, // First-party analytics OK advertising: false, // Disabled by GPC dataSale: false, // Disabled by GPC dataSharing: false, // Disabled by GPC gpcApplied: true, timestamp: new Date().toISOString() }; localStorage.setItem('consent_preferences', JSON.stringify(gpcConsent)); // Also set a specific GPC flag localStorage.setItem('gpc_honored', JSON.stringify({ honored: true, timestamp: this.state.timestamp, source: this.state.source })); } private async notifyBackendOfGPC(): Promise { try { await fetch('/api/v1/consent/gpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ gpcDetected: true, source: this.state.source, timestamp: this.state.timestamp, userAgent: navigator.userAgent, sessionId: this.getSessionId() }) }); } catch (error) { console.error('[GPC] Failed to notify backend:', error); } } private dispatchGPCEvent(): void { window.dispatchEvent(new CustomEvent('gpc:honored', { detail: { source: this.state.source, timestamp: this.state.timestamp } })); } private logDetection(): void { const log = { event: 'gpc_detection', detected: this.state.detected, source: this.state.source, timestamp: this.state.timestamp, honored: this.state.honored, url: window.location.href }; // Store locally for audit const logs = JSON.parse(localStorage.getItem('gpc_detection_logs') || '[]'); logs.push(log); // Keep last 100 logs if (logs.length > 100) { logs.shift(); } localStorage.setItem('gpc_detection_logs', JSON.stringify(logs)); } private getSessionId(): string { let sessionId = sessionStorage.getItem('privacy_session_id'); if (!sessionId) { sessionId = crypto.randomUUID(); sessionStorage.setItem('privacy_session_id', sessionId); } return sessionId; } // Subscribe to GPC state changes onGPCChange(listener: (state: GPCState) => void): () => void { this.detectionListeners.add(listener); return () => this.detectionListeners.delete(listener); } private notifyListeners(): void { this.detectionListeners.forEach(listener => { try { listener(this.state); } catch (error) { console.error('[GPC] Listener error:', error); } }); } getState(): GPCState { return { ...this.state }; } // Check if GPC should apply to this user/session shouldApplyGPC(): boolean { if (this.state.detected) return true; // Check if previously detected in this session if (this.config.applyToAllSessions) { const stored = localStorage.getItem('gpc_honored'); if (stored) { const { honored } = JSON.parse(stored); return honored; } } return false; } } // Initialize GPC detection on page load const initializeGPCDetection = async (): Promise => { const gpcService = new GPCDetectionService({ honorImmediately: true, logAllDetections: true, notifyBackend: true, applyToAllSessions: true }); // Detect before any scripts load await gpcService.detectGPC(); // Make available globally for other scripts (window as any).__GPCService__ = gpcService; }; // Run immediately if (typeof document !== 'undefined') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeGPCDetection); } else { initializeGPCDetection(); } } export { GPCDetectionService, GPCState, GPCComplianceConfig }; ``` ### Priority 3: Data Minimization Enforcement CPRA requires businesses to collect only the personal information that is "reasonably necessary and proportionate" to achieve the purposes disclosed to consumers. ```typescript // data-minimization-service.ts // Enforce CPRA data minimization requirements interface DataField { name: string; category: 'necessary' | 'functional' | 'analytics' | 'advertising'; purpose: string; retentionDays: number; sensitivePI: boolean; } interface CollectionPurpose { id: string; name: string; description: string; legalBasis: 'consent' | 'contract' | 'legitimate_interest'; requiredFields: string[]; optionalFields: string[]; } interface MinimizationConfig { purposes: CollectionPurpose[]; dataFields: DataField[]; strictMode: boolean; auditLogging: boolean; } class DataMinimizationService { private config: MinimizationConfig; private collectionLog: CollectionEvent[] = []; constructor(config: MinimizationConfig) { this.config = config; this.validateConfig(); } private validateConfig(): void { // Ensure all required fields are mapped to purposes this.config.purposes.forEach(purpose => { purpose.requiredFields.forEach(field => { const fieldDef = this.config.dataFields.find(f => f.name === field); if (!fieldDef) { console.warn(`[Minimization] Field "${field}" required by purpose "${purpose.id}" is not defined`); } }); }); } // Check if data collection is allowed for given purpose canCollect(fieldName: string, purposeId: string): CollectionDecision { const purpose = this.config.purposes.find(p => p.id === purposeId); const field = this.config.dataFields.find(f => f.name === fieldName); if (!purpose) { return { allowed: false, reason: `Unknown purpose: ${purposeId}`, requiresConsent: false }; } if (!field) { return { allowed: false, reason: `Unknown field: ${fieldName}`, requiresConsent: false }; } // Check if field is necessary for purpose const isRequired = purpose.requiredFields.includes(fieldName); const isOptional = purpose.optionalFields.includes(fieldName); if (!isRequired && !isOptional) { return { allowed: false, reason: `Field "${fieldName}" is not necessary for purpose "${purposeId}" `, requiresConsent: false, minimizationViolation: true }; } // Check consent requirements const requiresConsent = field.sensitivePI || field.category === 'advertising' || purpose.legalBasis === 'consent'; return { allowed: true, reason: isRequired ? 'Required for purpose' : 'Optional for purpose', requiresConsent, sensitivePI: field.sensitivePI, retentionDays: field.retentionDays }; } // Validate a data collection request validateCollection(data: Record, purposeId: string): ValidationResult { const issues: ValidationIssue[] = []; const allowedFields: string[] = []; const blockedFields: string[] = []; Object.keys(data).forEach(fieldName => { const decision = this.canCollect(fieldName, purposeId); if (decision.allowed) { allowedFields.push(fieldName); } else { blockedFields.push(fieldName); issues.push({ field: fieldName, reason: decision.reason, severity: decision.minimizationViolation ? 'error' : 'warning' }); } }); // Log the collection attempt if (this.config.auditLogging) { this.logCollectionAttempt({ purposeId, requestedFields: Object.keys(data), allowedFields, blockedFields, timestamp: new Date().toISOString() }); } return { valid: issues.filter(i => i.severity === 'error').length === 0, issues, allowedData: this.filterToAllowed(data, allowedFields), blockedFields }; } // Filter data to only allowed fields private filterToAllowed(data: Record, allowedFields: string[]): Record { const filtered: Record = {}; allowedFields.forEach(field => { if (data[field] !== undefined) { filtered[field] = data[field]; } }); return filtered; } // Sanitize data before collection (remove unnecessary fields) sanitizeForCollection(data: Record, purposeId: string): Record { const validation = this.validateCollection(data, purposeId); if (!validation.valid && this.config.strictMode) { throw new Error(`Data minimization violation: ${validation.issues.map(i => i.reason).join(', ')}`); } return validation.allowedData; } // Check retention requirements checkRetention(fieldName: string): RetentionInfo { const field = this.config.dataFields.find(f => f.name === fieldName); if (!field) { return { found: false, retentionDays: 0, deleteAfter: new Date() }; } const deleteAfter = new Date(); deleteAfter.setDate(deleteAfter.getDate() + field.retentionDays); return { found: true, retentionDays: field.retentionDays, deleteAfter, category: field.category, sensitivePI: field.sensitivePI }; } // Generate retention schedule for all data generateRetentionSchedule(): RetentionSchedule[] { return this.config.dataFields.map(field => { const deleteAfter = new Date(); deleteAfter.setDate(deleteAfter.getDate() + field.retentionDays); return { fieldName: field.name, category: field.category, retentionDays: field.retentionDays, deleteAfter: deleteAfter.toISOString(), sensitivePI: field.sensitivePI, purpose: field.purpose }; }).sort((a, b) => a.retentionDays - b.retentionDays); } // Log collection attempts for audit private logCollectionAttempt(event: CollectionEvent): void { this.collectionLog.push(event); // Also send to server for permanent audit trail this.sendAuditLog(event); } private async sendAuditLog(event: CollectionEvent): Promise { try { await fetch('/api/v1/audit/data-collection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }); } catch (error) { console.error('[Minimization] Failed to send audit log:', error); } } // Get audit log getCollectionLog(): CollectionEvent[] { return [...this.collectionLog]; } // Generate compliance report generateComplianceReport(): MinimizationReport { const totalAttempts = this.collectionLog.length; const violations = this.collectionLog.filter(e => e.blockedFields.length > 0); const fieldViolationCounts: Record = {}; violations.forEach(v => { v.blockedFields.forEach(field => { fieldViolationCounts[field] = (fieldViolationCounts[field] || 0) + 1; }); }); return { period: { start: this.collectionLog[0]?.timestamp || new Date().toISOString(), end: new Date().toISOString() }, totalCollectionAttempts: totalAttempts, violationCount: violations.length, violationRate: totalAttempts > 0 ? violations.length / totalAttempts : 0, topViolatedFields: Object.entries(fieldViolationCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([field, count]) => ({ field, count })), recommendations: this.generateRecommendations(fieldViolationCounts) }; } private generateRecommendations(violations: Record): string[] { const recommendations: string[] = []; if (Object.keys(violations).length > 0) { recommendations.push('Review and remove collection of frequently blocked fields'); } // Check for sensitive PI violations const sensitiveViolations = Object.keys(violations).filter(field => { const fieldDef = this.config.dataFields.find(f => f.name === field); return fieldDef?.sensitivePI; }); if (sensitiveViolations.length > 0) { recommendations.push(`Urgent: Review sensitive PI collection for fields: ${sensitiveViolations.join(', ')}`); } return recommendations; } } interface CollectionDecision { allowed: boolean; reason: string; requiresConsent: boolean; minimizationViolation?: boolean; sensitivePI?: boolean; retentionDays?: number; } interface ValidationIssue { field: string; reason: string; severity: 'error' | 'warning'; } interface ValidationResult { valid: boolean; issues: ValidationIssue[]; allowedData: Record; blockedFields: string[]; } interface RetentionInfo { found: boolean; retentionDays: number; deleteAfter: Date; category?: string; sensitivePI?: boolean; } interface RetentionSchedule { fieldName: string; category: string; retentionDays: number; deleteAfter: string; sensitivePI: boolean; purpose: string; } interface CollectionEvent { purposeId: string; requestedFields: string[]; allowedFields: string[]; blockedFields: string[]; timestamp: string; } interface MinimizationReport { period: { start: string; end: string }; totalCollectionAttempts: number; violationCount: number; violationRate: number; topViolatedFields: { field: string; count: number }[]; recommendations: string[]; } export { DataMinimizationService, MinimizationConfig, DataField, CollectionPurpose }; ``` ### Priority 4: Service Provider Contracts CPRA requires updated contracts with all service providers that process California consumer data. ```typescript // service-provider-contract-tracker.ts // Track and validate CPRA service provider contract requirements interface ServiceProvider { id: string; name: string; type: 'service_provider' | 'contractor' | 'third_party'; dataCategories: string[]; purposes: string[]; contractDate: string; contractExpiry: string; cpraCompliant: boolean; lastAuditDate?: string; } interface ContractRequirement { id: string; description: string; required: boolean; cpraSection: string; } interface ContractAudit { providerId: string; auditDate: string; requirements: ContractRequirementStatus[]; overallCompliant: boolean; findings: string[]; nextAuditDate: string; } interface ContractRequirementStatus { requirementId: string; status: 'met' | 'not_met' | 'partial' | 'not_applicable'; notes?: string; } class ServiceProviderContractTracker { private providers: Map = new Map(); private audits: Map = new Map(); // CPRA required contract provisions private readonly cpraRequirements: ContractRequirement[] = [ { id: 'purpose_limitation', description: 'Contract specifies and limits processing purposes', required: true, cpraSection: '1798.100(d)(1)' }, { id: 'no_selling', description: 'Prohibition on selling personal information', required: true, cpraSection: '1798.100(d)(2)' }, { id: 'no_sharing', description: 'Prohibition on sharing for cross-context behavioral advertising', required: true, cpraSection: '1798.100(d)(2)' }, { id: 'no_retention_beyond_purpose', description: 'Cannot retain data longer than necessary for specified purpose', required: true, cpraSection: '1798.100(d)(3)' }, { id: 'subcontractor_flow_down', description: 'Same obligations flow down to subcontractors', required: true, cpraSection: '1798.100(d)(4)' }, { id: 'business_audit_rights', description: 'Business has right to audit service provider compliance', required: true, cpraSection: '1798.100(d)(5)' }, { id: 'notify_inability_comply', description: 'Notify business if unable to meet CPRA obligations', required: true, cpraSection: '1798.100(d)(6)' }, { id: 'consumer_rights_assistance', description: 'Enable business to respond to consumer rights requests', required: true, cpraSection: '1798.100(d)(7)' }, { id: 'security_measures', description: 'Implement appropriate security measures', required: true, cpraSection: '1798.100(d)(8)' }, { id: 'breach_notification', description: 'Notify business of data breaches affecting consumer data', required: true, cpraSection: '1798.100(d)(9)' } ]; addProvider(provider: ServiceProvider): void { this.providers.set(provider.id, provider); this.audits.set(provider.id, []); } conductAudit(providerId: string, requirementStatuses: ContractRequirementStatus[]): ContractAudit { const provider = this.providers.get(providerId); if (!provider) { throw new Error(`Provider not found: ${providerId}`); } // Check all required requirements are addressed const requiredReqs = this.cpraRequirements.filter(r => r.required); const missingRequirements = requiredReqs.filter(req => !requirementStatuses.find(s => s.requirementId === req.id) ); if (missingRequirements.length > 0) { throw new Error(`Missing requirement statuses: ${missingRequirements.map(r => r.id).join(', ')}`); } // Determine overall compliance const nonCompliantRequired = requirementStatuses.filter(s => { const req = this.cpraRequirements.find(r => r.id === s.requirementId); return req?.required && s.status === 'not_met'; }); const overallCompliant = nonCompliantRequired.length === 0; // Generate findings const findings: string[] = []; requirementStatuses.forEach(status => { if (status.status === 'not_met') { const req = this.cpraRequirements.find(r => r.id === status.requirementId); findings.push(`${req?.description || status.requirementId}: Not met${status.notes ? ` - ${status.notes}` : ''}`); } else if (status.status === 'partial') { const req = this.cpraRequirements.find(r => r.id === status.requirementId); findings.push(`${req?.description || status.requirementId}: Partially met${status.notes ? ` - ${status.notes}` : ''}`); } }); // Calculate next audit date (annual minimum, 6 months if issues found) const nextAuditDate = new Date(); nextAuditDate.setMonth(nextAuditDate.getMonth() + (overallCompliant ? 12 : 6)); const audit: ContractAudit = { providerId, auditDate: new Date().toISOString(), requirements: requirementStatuses, overallCompliant, findings, nextAuditDate: nextAuditDate.toISOString() }; // Store audit const providerAudits = this.audits.get(providerId) || []; providerAudits.push(audit); this.audits.set(providerId, providerAudits); // Update provider compliance status provider.cpraCompliant = overallCompliant; provider.lastAuditDate = audit.auditDate; this.providers.set(providerId, provider); return audit; } getProviderStatus(providerId: string): ProviderComplianceStatus { const provider = this.providers.get(providerId); if (!provider) { throw new Error(`Provider not found: ${providerId}`); } const audits = this.audits.get(providerId) || []; const latestAudit = audits[audits.length - 1]; const isContractExpired = new Date(provider.contractExpiry) < new Date(); const needsAudit = !latestAudit || new Date(latestAudit.nextAuditDate) < new Date(); return { provider, latestAudit, isContractExpired, needsAudit, riskLevel: this.calculateRiskLevel(provider, latestAudit, isContractExpired, needsAudit) }; } private calculateRiskLevel( provider: ServiceProvider, latestAudit: ContractAudit | undefined, isContractExpired: boolean, needsAudit: boolean ): 'low' | 'medium' | 'high' | 'critical' { if (isContractExpired) return 'critical'; if (!latestAudit) return 'high'; if (!latestAudit.overallCompliant) return 'high'; if (needsAudit) return 'medium'; if (latestAudit.findings.length > 0) return 'medium'; return 'low'; } getAllProvidersStatus(): ProviderComplianceStatus[] { const statuses: ProviderComplianceStatus[] = []; this.providers.forEach((_, providerId) => { statuses.push(this.getProviderStatus(providerId)); }); return statuses.sort((a, b) => { const riskOrder = { critical: 0, high: 1, medium: 2, low: 3 }; return riskOrder[a.riskLevel] - riskOrder[b.riskLevel]; }); } generateComplianceReport(): ServiceProviderReport { const allStatuses = this.getAllProvidersStatus(); const byRiskLevel = { critical: allStatuses.filter(s => s.riskLevel === 'critical'), high: allStatuses.filter(s => s.riskLevel === 'high'), medium: allStatuses.filter(s => s.riskLevel === 'medium'), low: allStatuses.filter(s => s.riskLevel === 'low') }; const expiredContracts = allStatuses.filter(s => s.isContractExpired); const needingAudit = allStatuses.filter(s => s.needsAudit); const nonCompliant = allStatuses.filter(s => !s.provider.cpraCompliant); return { generatedAt: new Date().toISOString(), totalProviders: allStatuses.length, complianceRate: allStatuses.filter(s => s.provider.cpraCompliant).length / allStatuses.length, byRiskLevel: { critical: byRiskLevel.critical.length, high: byRiskLevel.high.length, medium: byRiskLevel.medium.length, low: byRiskLevel.low.length }, actionItems: [ ...expiredContracts.map(s => ({ priority: 'critical' as const, action: `Renew contract with ${s.provider.name}`, providerId: s.provider.id })), ...nonCompliant.map(s => ({ priority: 'high' as const, action: `Address compliance findings for ${s.provider.name}`, providerId: s.provider.id })), ...needingAudit.map(s => ({ priority: 'medium' as const, action: `Conduct audit for ${s.provider.name}`, providerId: s.provider.id })) ], requirements: this.cpraRequirements }; } exportForLegal(): string { const report = this.generateComplianceReport(); const statuses = this.getAllProvidersStatus(); let output = `# CPRA Service Provider Compliance Report\n`; output += `Generated: ${report.generatedAt}\n\n`; output += `## Summary\n`; output += `- Total Providers: ${report.totalProviders}\n`; output += `- Compliance Rate: ${(report.complianceRate * 100).toFixed(1)}%\n`; output += `- Critical Issues: ${report.byRiskLevel.critical}\n`; output += `- High Risk: ${report.byRiskLevel.high}\n\n`; output += `## Action Items\n`; report.actionItems.forEach(item => { output += `- [${item.priority.toUpperCase()}] ${item.action}\n`; }); output += `\n## Provider Details\n`; statuses.forEach(status => { output += `\n### ${status.provider.name}\n`; output += `- Type: ${status.provider.type}\n`; output += `- Risk Level: ${status.riskLevel}\n`; output += `- Contract Expiry: ${status.provider.contractExpiry}\n`; output += `- Last Audit: ${status.provider.lastAuditDate || 'Never'}\n`; if (status.latestAudit?.findings.length) { output += `- Findings:\n`; status.latestAudit.findings.forEach(f => { output += ` - ${f}\n`; }); } }); return output; } } interface ProviderComplianceStatus { provider: ServiceProvider; latestAudit?: ContractAudit; isContractExpired: boolean; needsAudit: boolean; riskLevel: 'low' | 'medium' | 'high' | 'critical'; } interface ServiceProviderReport { generatedAt: string; totalProviders: number; complianceRate: number; byRiskLevel: { critical: number; high: number; medium: number; low: number; }; actionItems: { priority: 'critical' | 'high' | 'medium' | 'low'; action: string; providerId: string; }[]; requirements: ContractRequirement[]; } export { ServiceProviderContractTracker, ServiceProvider, ContractAudit }; ``` ## Sensitive Personal Information Categories CPRA created a new category of "sensitive personal information" (SPI) with enhanced protections. Businesses must provide consumers with the right to limit the use of their SPI. ```typescript // sensitive-pi-handler.ts // Handle CPRA Sensitive Personal Information requirements type SensitivePICategory = | 'government_id' | 'financial_account' | 'precise_geolocation' | 'race_ethnicity' | 'religion' | 'union_membership' | 'genetic_data' | 'biometric_data' | 'health_data' | 'sex_life_orientation' | 'mail_email_text_content'; interface SensitivePIConfig { category: SensitivePICategory; displayName: string; description: string; examples: string[]; collectionPurposes: string[]; retentionDays: number; requiresExplicitConsent: boolean; canBeUsedForAdvertising: boolean; } interface SensitivePIConsent { category: SensitivePICategory; consented: boolean; purpose: string; timestamp: string; expiresAt?: string; } class SensitivePIHandler { private configs: Map; private consents: Map = new Map(); constructor() { this.configs = new Map([ ['government_id', { category: 'government_id', displayName: 'Government Identifiers', description: 'Social Security numbers, driver\'s license numbers, state ID numbers, passport numbers', examples: ['SSN', 'Driver\'s License', 'State ID', 'Passport Number'], collectionPurposes: ['identity_verification', 'legal_compliance'], retentionDays: 365, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['financial_account', { category: 'financial_account', displayName: 'Financial Information', description: 'Account numbers, debit/credit card numbers with security codes, account credentials', examples: ['Bank Account Numbers', 'Credit Card Numbers', 'Financial Credentials'], collectionPurposes: ['payment_processing', 'fraud_prevention'], retentionDays: 180, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['precise_geolocation', { category: 'precise_geolocation', displayName: 'Precise Geolocation', description: 'Geographic location data that can identify a consumer within a geographic area with radius of 1,850 feet or less', examples: ['GPS Coordinates', 'Precise Location Data'], collectionPurposes: ['location_services', 'delivery', 'emergency_services'], retentionDays: 30, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['race_ethnicity', { category: 'race_ethnicity', displayName: 'Racial or Ethnic Origin', description: 'Information revealing racial or ethnic origin', examples: ['Race', 'Ethnicity', 'National Origin'], collectionPurposes: ['diversity_reporting', 'equal_opportunity'], retentionDays: 365, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['religion', { category: 'religion', displayName: 'Religious Beliefs', description: 'Information revealing religious or philosophical beliefs', examples: ['Religion', 'Religious Affiliation', 'Philosophical Beliefs'], collectionPurposes: ['accommodation_requests', 'diversity_reporting'], retentionDays: 365, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['union_membership', { category: 'union_membership', displayName: 'Union Membership', description: 'Information about union membership', examples: ['Union Affiliation', 'Collective Bargaining Status'], collectionPurposes: ['employment_records', 'benefits_administration'], retentionDays: 365, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['genetic_data', { category: 'genetic_data', displayName: 'Genetic Data', description: 'Genetic information', examples: ['DNA Test Results', 'Genetic Markers', 'Hereditary Information'], collectionPurposes: ['medical_treatment', 'research_with_consent'], retentionDays: 365, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['biometric_data', { category: 'biometric_data', displayName: 'Biometric Information', description: 'Unique biometric data used for identification purposes', examples: ['Fingerprints', 'Face Geometry', 'Voiceprints', 'Retina Scans'], collectionPurposes: ['authentication', 'security_access'], retentionDays: 365, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['health_data', { category: 'health_data', displayName: 'Health Information', description: 'Information concerning a consumer\'s health', examples: ['Medical Conditions', 'Treatments', 'Health Records'], collectionPurposes: ['medical_treatment', 'insurance', 'wellness_programs'], retentionDays: 365, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['sex_life_orientation', { category: 'sex_life_orientation', displayName: 'Sex Life or Sexual Orientation', description: 'Information about sex life or sexual orientation', examples: ['Sexual Orientation', 'Gender Identity'], collectionPurposes: ['diversity_reporting', 'accommodation_requests'], retentionDays: 365, requiresExplicitConsent: true, canBeUsedForAdvertising: false }], ['mail_email_text_content', { category: 'mail_email_text_content', displayName: 'Private Communications', description: 'Contents of mail, email, and text messages (unless business is intended recipient)', examples: ['Email Content', 'Text Message Content', 'Private Messages'], collectionPurposes: ['communication_services', 'customer_support'], retentionDays: 90, requiresExplicitConsent: true, canBeUsedForAdvertising: false }] ]); } // Check if data is sensitive PI classifyData(fieldName: string, value: any): SensitivePICategory | null { const fieldLower = fieldName.toLowerCase(); // Government ID detection if (fieldLower.includes('ssn') || fieldLower.includes('social_security') || fieldLower.includes('drivers_license') || fieldLower.includes('passport')) { return 'government_id'; } // Financial data detection if (fieldLower.includes('card_number') || fieldLower.includes('account_number') || fieldLower.includes('cvv') || fieldLower.includes('routing_number')) { return 'financial_account'; } // Geolocation detection if (fieldLower.includes('latitude') || fieldLower.includes('longitude') || fieldLower.includes('gps') || fieldLower.includes('precise_location')) { // Check if precision indicates "precise" (within 1850 feet) if (this.isPreciseLocation(value)) { return 'precise_geolocation'; } } // Health data detection if (fieldLower.includes('diagnosis') || fieldLower.includes('medical') || fieldLower.includes('health_condition') || fieldLower.includes('treatment')) { return 'health_data'; } // Biometric detection if (fieldLower.includes('fingerprint') || fieldLower.includes('face_scan') || fieldLower.includes('voiceprint') || fieldLower.includes('retina')) { return 'biometric_data'; } // Race/ethnicity detection if (fieldLower.includes('race') || fieldLower.includes('ethnicity') || fieldLower.includes('national_origin')) { return 'race_ethnicity'; } return null; } private isPreciseLocation(value: any): boolean { // If lat/lng with more than 3 decimal places, it's "precise" // 3 decimal places = ~111m precision (within 1850 feet threshold) if (typeof value === 'object' && value.latitude && value.longitude) { const latDecimals = (value.latitude.toString().split('.')[1] || '').length; const lngDecimals = (value.longitude.toString().split('.')[1] || '').length; return latDecimals > 3 || lngDecimals > 3; } return false; } // Record consent for sensitive PI recordConsent(userId: string, consent: SensitivePIConsent): void { const userConsents = this.consents.get(userId) || []; // Remove any existing consent for same category/purpose const filtered = userConsents.filter(c => !(c.category === consent.category && c.purpose === consent.purpose) ); filtered.push(consent); this.consents.set(userId, filtered); } // Check if user has consented to sensitive PI collection hasConsent(userId: string, category: SensitivePICategory, purpose: string): boolean { const userConsents = this.consents.get(userId) || []; const consent = userConsents.find(c => c.category === category && c.purpose === purpose && c.consented === true ); if (!consent) return false; // Check if expired if (consent.expiresAt && new Date(consent.expiresAt) < new Date()) { return false; } return true; } // Get all sensitive PI categories getAllCategories(): SensitivePIConfig[] { return Array.from(this.configs.values()); } // Generate disclosure text for privacy notice generateDisclosure(): string { let disclosure = '## Sensitive Personal Information We Collect\n\n'; disclosure += 'We may collect the following categories of sensitive personal information:\n\n'; this.configs.forEach(config => { disclosure += `### ${config.displayName}\n`; disclosure += `${config.description}\n\n`; disclosure += `**Examples**: ${config.examples.join(', ')}\n\n`; disclosure += `**Purposes**: ${config.collectionPurposes.join(', ')}\n\n`; disclosure += `**Retention Period**: ${config.retentionDays} days\n\n`; }); disclosure += '\n## Your Rights Regarding Sensitive Personal Information\n\n'; disclosure += 'You have the right to limit the use and disclosure of your sensitive personal information. '; disclosure += 'To exercise this right, click the "Limit Use of My Sensitive Personal Information" link '; disclosure += 'in the footer of our website or contact us using the information provided in this policy.\n'; return disclosure; } // Validate data collection request validateCollection(userId: string, data: Record): SPIValidationResult { const sensitiveFields: { field: string; category: SensitivePICategory }[] = []; const blockedFields: { field: string; category: SensitivePICategory; reason: string }[] = []; const allowedFields: string[] = []; Object.entries(data).forEach(([field, value]) => { const category = this.classifyData(field, value); if (category) { sensitiveFields.push({ field, category }); const config = this.configs.get(category)!; // Check if any valid purpose has consent const hasValidConsent = config.collectionPurposes.some(purpose => this.hasConsent(userId, category, purpose) ); if (!hasValidConsent && config.requiresExplicitConsent) { blockedFields.push({ field, category, reason: `Explicit consent required for ${config.displayName}` }); } else { allowedFields.push(field); } } else { allowedFields.push(field); } }); return { valid: blockedFields.length === 0, sensitiveFields, blockedFields, allowedFields, requiresConsentPrompt: blockedFields.length > 0 }; } // Generate consent request for blocked fields generateConsentRequest(blockedFields: { field: string; category: SensitivePICategory }[]): ConsentRequest { const categories = [...new Set(blockedFields.map(f => f.category))]; return { id: crypto.randomUUID(), type: 'sensitive_pi', categories: categories.map(cat => { const config = this.configs.get(cat)!; return { category: cat, displayName: config.displayName, description: config.description, purposes: config.collectionPurposes }; }), createdAt: new Date().toISOString() }; } } interface SPIValidationResult { valid: boolean; sensitiveFields: { field: string; category: SensitivePICategory }[]; blockedFields: { field: string; category: SensitivePICategory; reason: string }[]; allowedFields: string[]; requiresConsentPrompt: boolean; } interface ConsentRequest { id: string; type: string; categories: { category: SensitivePICategory; displayName: string; description: string; purposes: string[]; }[]; createdAt: string; } export { SensitivePIHandler, SensitivePICategory, SensitivePIConfig }; ``` ## CPRA Compliance Checklist Implementation Here's a comprehensive checklist system for tracking CPRA compliance: ```typescript // cpra-compliance-checklist.ts // Complete CPRA compliance tracking system interface ChecklistItem { id: string; category: string; requirement: string; cpraSection: string; status: 'not_started' | 'in_progress' | 'completed' | 'not_applicable'; notes: string; dueDate?: string; completedDate?: string; assignee?: string; evidence?: string[]; } interface ComplianceCategory { name: string; description: string; items: ChecklistItem[]; } class CPRAComplianceChecklist { private categories: ComplianceCategory[] = [ { name: 'Privacy Notice Updates', description: 'Required disclosures in privacy policy', items: [ { id: 'pn_1', category: 'Privacy Notice Updates', requirement: 'Include all required disclosures per 1798.100', cpraSection: '1798.100(a)', status: 'not_started', notes: '' }, { id: 'pn_2', category: 'Privacy Notice Updates', requirement: 'List categories of PI collected in preceding 12 months', cpraSection: '1798.110(c)(1)', status: 'not_started', notes: '' }, { id: 'pn_3', category: 'Privacy Notice Updates', requirement: 'Describe business purposes for each category', cpraSection: '1798.110(c)(2)', status: 'not_started', notes: '' }, { id: 'pn_4', category: 'Privacy Notice Updates', requirement: 'List categories of sensitive PI collected', cpraSection: '1798.121', status: 'not_started', notes: '' }, { id: 'pn_5', category: 'Privacy Notice Updates', requirement: 'Explain consumer rights and how to exercise them', cpraSection: '1798.135', status: 'not_started', notes: '' }, { id: 'pn_6', category: 'Privacy Notice Updates', requirement: 'Provide opt-out links (Do Not Sell/Share)', cpraSection: '1798.135(a)', status: 'not_started', notes: '' }, { id: 'pn_7', category: 'Privacy Notice Updates', requirement: 'Include retention periods for each PI category', cpraSection: '1798.100(a)(3)', status: 'not_started', notes: '' } ] }, { name: 'Consumer Rights', description: 'Mechanisms for handling consumer rights requests', items: [ { id: 'cr_1', category: 'Consumer Rights', requirement: 'Right to Know - provide access mechanism', cpraSection: '1798.110', status: 'not_started', notes: '' }, { id: 'cr_2', category: 'Consumer Rights', requirement: 'Right to Delete - implement deletion process', cpraSection: '1798.105', status: 'not_started', notes: '' }, { id: 'cr_3', category: 'Consumer Rights', requirement: 'Right to Correct - enable data correction', cpraSection: '1798.106', status: 'not_started', notes: '' }, { id: 'cr_4', category: 'Consumer Rights', requirement: 'Right to Opt-Out of Sale/Sharing', cpraSection: '1798.120', status: 'not_started', notes: '' }, { id: 'cr_5', category: 'Consumer Rights', requirement: 'Right to Limit Use of Sensitive PI', cpraSection: '1798.121', status: 'not_started', notes: '' }, { id: 'cr_6', category: 'Consumer Rights', requirement: 'Right to Data Portability', cpraSection: '1798.130', status: 'not_started', notes: '' }, { id: 'cr_7', category: 'Consumer Rights', requirement: 'Respond within 45 days (extendable to 90)', cpraSection: '1798.130(a)(2)', status: 'not_started', notes: '' }, { id: 'cr_8', category: 'Consumer Rights', requirement: 'Verify consumer identity before fulfilling requests', cpraSection: '1798.140', status: 'not_started', notes: '' } ] }, { name: 'Opt-Out Mechanisms', description: 'Technical implementations for opt-out rights', items: [ { id: 'oo_1', category: 'Opt-Out Mechanisms', requirement: 'Implement GPC signal detection', cpraSection: '1798.135(e)', status: 'not_started', notes: '' }, { id: 'oo_2', category: 'Opt-Out Mechanisms', requirement: 'Honor GPC as valid opt-out request', cpraSection: '1798.135(e)', status: 'not_started', notes: '' }, { id: 'oo_3', category: 'Opt-Out Mechanisms', requirement: '"Do Not Sell or Share" link on homepage', cpraSection: '1798.135(a)(1)', status: 'not_started', notes: '' }, { id: 'oo_4', category: 'Opt-Out Mechanisms', requirement: '"Limit Use of Sensitive PI" link if applicable', cpraSection: '1798.135(a)(2)', status: 'not_started', notes: '' }, { id: 'oo_5', category: 'Opt-Out Mechanisms', requirement: 'No dark patterns in opt-out process', cpraSection: '1798.140(l)', status: 'not_started', notes: '' }, { id: 'oo_6', category: 'Opt-Out Mechanisms', requirement: 'Opt-out must be as easy as opt-in', cpraSection: '1798.185(a)(4)(C)', status: 'not_started', notes: '' } ] }, { name: 'Service Provider Contracts', description: 'Contractual requirements with vendors', items: [ { id: 'sp_1', category: 'Service Provider Contracts', requirement: 'Written contracts with all service providers', cpraSection: '1798.100(d)', status: 'not_started', notes: '' }, { id: 'sp_2', category: 'Service Provider Contracts', requirement: 'Contracts prohibit selling/sharing consumer PI', cpraSection: '1798.100(d)(2)', status: 'not_started', notes: '' }, { id: 'sp_3', category: 'Service Provider Contracts', requirement: 'Contracts limit processing to specified purposes', cpraSection: '1798.100(d)(1)', status: 'not_started', notes: '' }, { id: 'sp_4', category: 'Service Provider Contracts', requirement: 'Flow-down provisions to subcontractors', cpraSection: '1798.100(d)(4)', status: 'not_started', notes: '' }, { id: 'sp_5', category: 'Service Provider Contracts', requirement: 'Audit rights included in contracts', cpraSection: '1798.100(d)(5)', status: 'not_started', notes: '' } ] }, { name: 'Data Minimization', description: 'Collection and retention limitations', items: [ { id: 'dm_1', category: 'Data Minimization', requirement: 'Collect only PI reasonably necessary for disclosed purposes', cpraSection: '1798.100(c)', status: 'not_started', notes: '' }, { id: 'dm_2', category: 'Data Minimization', requirement: 'Retain PI only as long as necessary', cpraSection: '1798.100(a)(3)', status: 'not_started', notes: '' }, { id: 'dm_3', category: 'Data Minimization', requirement: 'Document retention periods for each PI category', cpraSection: '1798.100(a)(3)', status: 'not_started', notes: '' }, { id: 'dm_4', category: 'Data Minimization', requirement: 'Implement automated data deletion', cpraSection: '1798.100(c)', status: 'not_started', notes: '' } ] }, { name: 'Security Measures', description: 'Data protection requirements', items: [ { id: 'sec_1', category: 'Security Measures', requirement: 'Implement reasonable security procedures', cpraSection: '1798.100(e)', status: 'not_started', notes: '' }, { id: 'sec_2', category: 'Security Measures', requirement: 'Conduct annual security assessments', cpraSection: '1798.185(a)(15)', status: 'not_started', notes: '' },

Usein kysytyt kysymykset

Is GPC mandatory under CPRA?
Yes. The California Attorney General and the CPPA have both confirmed that treating the Global Privacy Control (GPC) signal as a valid consumer request to opt-out of sale/sharing is mandatory.
What qualifies as sensitive personal information in California?
Sensitive PI includes social security numbers, financial account logins, precise geolocation, racial/ethnic origin, religious beliefs, genetic data, biometric data, health data, and sexual orientation.
R

Rachel Torres, Privacy Counsel

Kirjoittaja GetCookiesissa, erikoistunut tietosuojavaatimustenmukaisuuteen, hyväksyntähallintaan ja digitaalisen markkinoinnin optimointiin.

Valmis yksinkertaistamaan evästehyväksyntää?

GetCookies tekee GDPR:n, CCPA:n ja maailmanlaajuisen tietosuojavaatimustenmukaisuuden vaivattomaksi. Aloita tänään.