SOC 2 Type II Certification for ChatGPT Apps: Enterprise Security Compliance Guide

When enterprise customers evaluate ChatGPT applications for integration into their systems, SOC 2 Type II certification is often a non-negotiable requirement. This comprehensive guide walks you through achieving and maintaining SOC 2 compliance for your ChatGPT apps, covering all five Trust Service Criteria and practical implementation strategies.

Understanding SOC 2: Type I vs Type II

SOC 2 Type I provides a point-in-time assessment of your security controls. An auditor examines whether your controls are suitably designed on a specific date. This certification is useful for early-stage companies establishing their security posture.

SOC 2 Type II requires continuous monitoring over 6-12 months. Auditors evaluate not only the design of your controls but also their operating effectiveness throughout the observation period. Enterprise customers prefer Type II because it demonstrates sustained commitment to security.

The AICPA SOC 2 framework defines five Trust Service Criteria:

  1. Security - Protection against unauthorized access
  2. Availability - System accessibility for operation and use
  3. Processing Integrity - Complete, valid, accurate, timely processing
  4. Confidentiality - Protection of confidential information
  5. Privacy - Collection, use, retention, and disclosure of personal information

For ChatGPT applications handling sensitive customer data, implementing these criteria requires architectural decisions made during development, not bolted on later. This is where platforms like MakeAIHQ provide advantages - security controls are embedded in the application generation process.

Enterprise customers require SOC 2 Type II because it provides independent verification that your ChatGPT app maintains security controls consistently over time. Without this certification, you'll struggle to close deals with Fortune 500 companies, healthcare organizations, and financial institutions.

Security Criteria: Access Controls and Network Protection

The Security criterion is foundational to SOC 2 compliance. For ChatGPT applications, this means implementing robust access controls, network security measures, and change management procedures.

Role-Based Access Control (RBAC)

RBAC ensures users access only the resources necessary for their role. Here's a production-ready implementation for ChatGPT MCP servers:

// rbac-manager.ts - SOC 2 compliant access control system
import { createHash } from 'crypto';
import { EventEmitter } from 'events';

interface Permission {
  resource: string;
  action: 'read' | 'write' | 'delete' | 'admin';
  conditions?: Record<string, any>;
}

interface Role {
  id: string;
  name: string;
  permissions: Permission[];
  createdAt: Date;
  modifiedAt: Date;
}

interface User {
  id: string;
  email: string;
  roles: string[];
  mfaEnabled: boolean;
  lastLogin?: Date;
}

interface AccessLogEntry {
  timestamp: Date;
  userId: string;
  action: string;
  resource: string;
  granted: boolean;
  ipAddress: string;
  userAgent: string;
}

class RBACManager extends EventEmitter {
  private roles: Map<string, Role> = new Map();
  private users: Map<string, User> = new Map();
  private accessLog: AccessLogEntry[] = [];
  private sessionTokens: Map<string, { userId: string; expiresAt: Date }> = new Map();

  constructor() {
    super();
    this.initializeDefaultRoles();
    this.startSessionCleanup();
  }

  private initializeDefaultRoles(): void {
    // Admin role - full access
    this.roles.set('admin', {
      id: 'admin',
      name: 'Administrator',
      permissions: [
        { resource: '*', action: 'admin' }
      ],
      createdAt: new Date(),
      modifiedAt: new Date()
    });

    // Developer role - read/write app resources
    this.roles.set('developer', {
      id: 'developer',
      name: 'Developer',
      permissions: [
        { resource: 'apps', action: 'read' },
        { resource: 'apps', action: 'write' },
        { resource: 'tools', action: 'read' },
        { resource: 'tools', action: 'write' },
        { resource: 'analytics', action: 'read' }
      ],
      createdAt: new Date(),
      modifiedAt: new Date()
    });

    // Viewer role - read-only access
    this.roles.set('viewer', {
      id: 'viewer',
      name: 'Viewer',
      permissions: [
        { resource: 'apps', action: 'read' },
        { resource: 'analytics', action: 'read' }
      ],
      createdAt: new Date(),
      modifiedAt: new Date()
    });
  }

  // Authenticate user with MFA requirement
  async authenticate(
    email: string,
    password: string,
    mfaToken?: string,
    context?: { ipAddress: string; userAgent: string }
  ): Promise<string | null> {
    const user = Array.from(this.users.values()).find(u => u.email === email);

    if (!user) {
      this.logAccess({
        timestamp: new Date(),
        userId: email,
        action: 'login',
        resource: 'authentication',
        granted: false,
        ipAddress: context?.ipAddress || 'unknown',
        userAgent: context?.userAgent || 'unknown'
      });
      return null;
    }

    // Verify MFA if enabled (required for SOC 2)
    if (user.mfaEnabled && !mfaToken) {
      throw new Error('MFA token required');
    }

    // Generate session token
    const token = this.generateSecureToken(user.id);
    const expiresAt = new Date(Date.now() + 8 * 60 * 60 * 1000); // 8 hours

    this.sessionTokens.set(token, { userId: user.id, expiresAt });
    user.lastLogin = new Date();

    this.logAccess({
      timestamp: new Date(),
      userId: user.id,
      action: 'login',
      resource: 'authentication',
      granted: true,
      ipAddress: context?.ipAddress || 'unknown',
      userAgent: context?.userAgent || 'unknown'
    });

    this.emit('user:authenticated', { userId: user.id, timestamp: new Date() });
    return token;
  }

  // Check if user has permission for specific action
  async authorize(
    sessionToken: string,
    resource: string,
    action: Permission['action'],
    context?: { ipAddress: string; userAgent: string }
  ): Promise<boolean> {
    const session = this.sessionTokens.get(sessionToken);

    if (!session || session.expiresAt < new Date()) {
      this.logAccess({
        timestamp: new Date(),
        userId: 'unknown',
        action,
        resource,
        granted: false,
        ipAddress: context?.ipAddress || 'unknown',
        userAgent: context?.userAgent || 'unknown'
      });
      return false;
    }

    const user = this.users.get(session.userId);
    if (!user) return false;

    // Check all user roles for permission
    const hasPermission = user.roles.some(roleId => {
      const role = this.roles.get(roleId);
      if (!role) return false;

      return role.permissions.some(perm => {
        // Wildcard admin access
        if (perm.resource === '*' && perm.action === 'admin') return true;

        // Exact match
        if (perm.resource === resource && perm.action === action) return true;

        // Admin action includes all other actions
        if (perm.resource === resource && perm.action === 'admin') return true;

        return false;
      });
    });

    this.logAccess({
      timestamp: new Date(),
      userId: user.id,
      action,
      resource,
      granted: hasPermission,
      ipAddress: context?.ipAddress || 'unknown',
      userAgent: context?.userAgent || 'unknown'
    });

    if (!hasPermission) {
      this.emit('access:denied', { userId: user.id, resource, action });
    }

    return hasPermission;
  }

  // Add user with roles
  addUser(user: User): void {
    this.users.set(user.id, user);
    this.emit('user:created', { userId: user.id, roles: user.roles });
  }

  // Enable MFA for user (required for privileged accounts)
  enableMFA(userId: string): void {
    const user = this.users.get(userId);
    if (user) {
      user.mfaEnabled = true;
      this.emit('user:mfa_enabled', { userId });
    }
  }

  // Revoke session token
  revokeSession(sessionToken: string): void {
    const session = this.sessionTokens.get(sessionToken);
    if (session) {
      this.sessionTokens.delete(sessionToken);
      this.emit('session:revoked', { userId: session.userId });
    }
  }

  // Get access logs for audit (SOC 2 requirement)
  getAccessLogs(startDate?: Date, endDate?: Date): AccessLogEntry[] {
    let logs = this.accessLog;

    if (startDate) {
      logs = logs.filter(log => log.timestamp >= startDate);
    }

    if (endDate) {
      logs = logs.filter(log => log.timestamp <= endDate);
    }

    return logs;
  }

  private generateSecureToken(userId: string): string {
    const randomBytes = Math.random().toString(36).substring(2, 15);
    const timestamp = Date.now().toString();
    return createHash('sha256')
      .update(`${userId}${randomBytes}${timestamp}`)
      .digest('hex');
  }

  private logAccess(entry: AccessLogEntry): void {
    this.accessLog.push(entry);

    // Retain logs for 1 year (SOC 2 requirement)
    const oneYearAgo = new Date();
    oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
    this.accessLog = this.accessLog.filter(log => log.timestamp > oneYearAgo);
  }

  private startSessionCleanup(): void {
    // Clean up expired sessions every hour
    setInterval(() => {
      const now = new Date();
      for (const [token, session] of this.sessionTokens.entries()) {
        if (session.expiresAt < now) {
          this.sessionTokens.delete(token);
        }
      }
    }, 60 * 60 * 1000);
  }
}

export { RBACManager, Permission, Role, User, AccessLogEntry };

This RBAC implementation provides several SOC 2-critical features:

  • Multi-factor authentication enforcement for privileged accounts
  • Session management with automatic expiration
  • Comprehensive access logging retained for 1 year
  • Role-based permissions with least privilege principle
  • Audit trail events for security monitoring

Network Security Controls

Implement network security controls including firewall rules, intrusion detection, and encrypted communications. For ChatGPT apps, ensure all API communications use TLS 1.3 minimum and implement rate limiting to prevent abuse.

Learn more about comprehensive security strategies in our ChatGPT App Security Best Practices guide.

Availability Criteria: Uptime and Disaster Recovery

The Availability criterion requires your ChatGPT application to maintain agreed-upon uptime SLAs (typically 99.9% or higher). This involves proactive monitoring, incident response procedures, and disaster recovery planning.

Health Check and Monitoring System

Implement comprehensive health checks that monitor critical system components:

// health-monitor.ts - SOC 2 compliant availability monitoring
import { EventEmitter } from 'events';

interface HealthCheck {
  name: string;
  check: () => Promise<{ healthy: boolean; details?: string }>;
  interval: number; // milliseconds
  timeout: number; // milliseconds
  critical: boolean; // affects overall system health
}

interface HealthStatus {
  timestamp: Date;
  healthy: boolean;
  checks: {
    name: string;
    status: 'healthy' | 'degraded' | 'down';
    responseTime: number;
    details?: string;
  }[];
  uptime: number; // percentage over last 24 hours
}

interface IncidentRecord {
  id: string;
  startTime: Date;
  endTime?: Date;
  affectedComponent: string;
  severity: 'critical' | 'major' | 'minor';
  resolved: boolean;
  rootCause?: string;
}

class HealthMonitor extends EventEmitter {
  private checks: Map<string, HealthCheck> = new Map();
  private healthHistory: { timestamp: Date; healthy: boolean }[] = [];
  private incidents: IncidentRecord[] = [];
  private monitoringIntervals: Map<string, NodeJS.Timeout> = new Map();

  constructor() {
    super();
    this.initializeDefaultChecks();
  }

  private initializeDefaultChecks(): void {
    // Database connectivity check
    this.addHealthCheck({
      name: 'database',
      check: async () => {
        try {
          // Simulate database ping
          const start = Date.now();
          // In production: await db.ping()
          await new Promise(resolve => setTimeout(resolve, 10));
          const elapsed = Date.now() - start;

          return {
            healthy: elapsed < 1000,
            details: `Response time: ${elapsed}ms`
          };
        } catch (error) {
          return {
            healthy: false,
            details: error instanceof Error ? error.message : 'Unknown error'
          };
        }
      },
      interval: 30000, // 30 seconds
      timeout: 5000,
      critical: true
    });

    // API endpoint check
    this.addHealthCheck({
      name: 'api',
      check: async () => {
        try {
          const start = Date.now();
          // In production: await fetch('https://api.example.com/health')
          await new Promise(resolve => setTimeout(resolve, 20));
          const elapsed = Date.now() - start;

          return {
            healthy: elapsed < 2000,
            details: `Response time: ${elapsed}ms`
          };
        } catch (error) {
          return {
            healthy: false,
            details: error instanceof Error ? error.message : 'Unknown error'
          };
        }
      },
      interval: 60000, // 1 minute
      timeout: 10000,
      critical: true
    });

    // ChatGPT API connectivity
    this.addHealthCheck({
      name: 'chatgpt_api',
      check: async () => {
        try {
          const start = Date.now();
          // In production: await openai.models.list()
          await new Promise(resolve => setTimeout(resolve, 50));
          const elapsed = Date.now() - start;

          return {
            healthy: elapsed < 3000,
            details: `Response time: ${elapsed}ms`
          };
        } catch (error) {
          return {
            healthy: false,
            details: error instanceof Error ? error.message : 'Unknown error'
          };
        }
      },
      interval: 120000, // 2 minutes
      timeout: 15000,
      critical: true
    });
  }

  addHealthCheck(check: HealthCheck): void {
    this.checks.set(check.name, check);
    this.startMonitoring(check.name);
  }

  private startMonitoring(checkName: string): void {
    const check = this.checks.get(checkName);
    if (!check) return;

    const interval = setInterval(async () => {
      await this.runHealthCheck(checkName);
    }, check.interval);

    this.monitoringIntervals.set(checkName, interval);
  }

  private async runHealthCheck(checkName: string): Promise<void> {
    const check = this.checks.get(checkName);
    if (!check) return;

    const start = Date.now();

    try {
      const timeoutPromise = new Promise<never>((_, reject) => {
        setTimeout(() => reject(new Error('Health check timeout')), check.timeout);
      });

      const result = await Promise.race([
        check.check(),
        timeoutPromise
      ]);

      const responseTime = Date.now() - start;

      if (!result.healthy && check.critical) {
        this.createIncident(checkName, 'critical');
      }

      this.emit('health:check', {
        name: checkName,
        healthy: result.healthy,
        responseTime,
        details: result.details
      });

    } catch (error) {
      if (check.critical) {
        this.createIncident(checkName, 'critical');
      }

      this.emit('health:check', {
        name: checkName,
        healthy: false,
        responseTime: Date.now() - start,
        details: error instanceof Error ? error.message : 'Unknown error'
      });
    }
  }

  async getHealthStatus(): Promise<HealthStatus> {
    const checkResults = await Promise.all(
      Array.from(this.checks.entries()).map(async ([name, check]) => {
        const start = Date.now();
        try {
          const result = await check.check();
          return {
            name,
            status: result.healthy ? 'healthy' : 'down' as const,
            responseTime: Date.now() - start,
            details: result.details
          };
        } catch (error) {
          return {
            name,
            status: 'down' as const,
            responseTime: Date.now() - start,
            details: error instanceof Error ? error.message : 'Unknown error'
          };
        }
      })
    );

    const allHealthy = checkResults.every(r => r.status === 'healthy');
    const uptime = this.calculateUptime();

    const status: HealthStatus = {
      timestamp: new Date(),
      healthy: allHealthy,
      checks: checkResults,
      uptime
    };

    this.healthHistory.push({
      timestamp: new Date(),
      healthy: allHealthy
    });

    // Keep 7 days of history
    const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
    this.healthHistory = this.healthHistory.filter(h => h.timestamp > sevenDaysAgo);

    return status;
  }

  private calculateUptime(): number {
    if (this.healthHistory.length === 0) return 100;

    const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
    const recentHistory = this.healthHistory.filter(h => h.timestamp > oneDayAgo);

    if (recentHistory.length === 0) return 100;

    const healthyCount = recentHistory.filter(h => h.healthy).length;
    return (healthyCount / recentHistory.length) * 100;
  }

  private createIncident(component: string, severity: IncidentRecord['severity']): void {
    const incident: IncidentRecord = {
      id: `INC-${Date.now()}`,
      startTime: new Date(),
      affectedComponent: component,
      severity,
      resolved: false
    };

    this.incidents.push(incident);
    this.emit('incident:created', incident);
  }

  resolveIncident(incidentId: string, rootCause: string): void {
    const incident = this.incidents.find(i => i.id === incidentId);
    if (incident && !incident.resolved) {
      incident.endTime = new Date();
      incident.resolved = true;
      incident.rootCause = rootCause;
      this.emit('incident:resolved', incident);
    }
  }

  getIncidentReport(startDate?: Date): IncidentRecord[] {
    let incidents = this.incidents;

    if (startDate) {
      incidents = incidents.filter(i => i.startTime >= startDate);
    }

    return incidents;
  }

  stop(): void {
    for (const interval of this.monitoringIntervals.values()) {
      clearInterval(interval);
    }
    this.monitoringIntervals.clear();
  }
}

export { HealthMonitor, HealthCheck, HealthStatus, IncidentRecord };

This monitoring system provides SOC 2-required capabilities:

  • Continuous availability monitoring for critical components
  • Automatic incident detection and tracking
  • Uptime calculation over configurable periods
  • Health status reporting for audit evidence
  • Incident response documentation with root cause analysis

Disaster Recovery Planning

Document and test your disaster recovery procedures quarterly. For ChatGPT applications, this includes:

  • Database backups with point-in-time recovery capability
  • Multi-region deployment for geographic redundancy
  • Failover procedures tested at least annually
  • Recovery Time Objective (RTO) and Recovery Point Objective (RPO) metrics

Learn more in our Incident Response Planning guide.

Processing Integrity: Data Validation and Quality Controls

Processing Integrity ensures your ChatGPT application processes data completely, accurately, and in a timely manner. This is particularly important for applications handling financial transactions, healthcare data, or legal documents.

Processing Integrity Validator

// processing-integrity-validator.ts - SOC 2 compliant data processing
import { EventEmitter } from 'events';
import { createHash } from 'crypto';

interface ValidationRule {
  field: string;
  required: boolean;
  type?: 'string' | 'number' | 'boolean' | 'object' | 'array';
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
  custom?: (value: any) => boolean;
}

interface ProcessingRecord {
  id: string;
  timestamp: Date;
  operation: string;
  inputHash: string;
  outputHash: string;
  status: 'success' | 'failure' | 'partial';
  validationErrors?: string[];
  processingTime: number;
}

class ProcessingIntegrityValidator extends EventEmitter {
  private validationRules: Map<string, ValidationRule[]> = new Map();
  private processingLog: ProcessingRecord[] = [];

  constructor() {
    super();
    this.initializeDefaultRules();
  }

  private initializeDefaultRules(): void {
    // ChatGPT tool call validation
    this.validationRules.set('tool_call', [
      { field: 'tool_name', required: true, type: 'string', minLength: 1, maxLength: 100 },
      { field: 'parameters', required: true, type: 'object' },
      { field: 'user_id', required: true, type: 'string', pattern: /^[a-zA-Z0-9_-]+$/ }
    ]);

    // User data validation
    this.validationRules.set('user_data', [
      { field: 'email', required: true, type: 'string', pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
      { field: 'organization', required: false, type: 'string', maxLength: 200 },
      { field: 'role', required: true, type: 'string', custom: (v) => ['admin', 'developer', 'viewer'].includes(v) }
    ]);
  }

  // Validate data against schema
  validate(schema: string, data: any): { valid: boolean; errors: string[] } {
    const rules = this.validationRules.get(schema);
    if (!rules) {
      return { valid: false, errors: [`Unknown schema: ${schema}`] };
    }

    const errors: string[] = [];

    for (const rule of rules) {
      const value = data[rule.field];

      // Required field check
      if (rule.required && (value === undefined || value === null)) {
        errors.push(`Field '${rule.field}' is required`);
        continue;
      }

      // Skip further validation if field is optional and not provided
      if (!rule.required && (value === undefined || value === null)) {
        continue;
      }

      // Type validation
      if (rule.type) {
        const actualType = Array.isArray(value) ? 'array' : typeof value;
        if (actualType !== rule.type) {
          errors.push(`Field '${rule.field}' must be of type ${rule.type}, got ${actualType}`);
          continue;
        }
      }

      // String length validation
      if (rule.type === 'string') {
        if (rule.minLength && value.length < rule.minLength) {
          errors.push(`Field '${rule.field}' must be at least ${rule.minLength} characters`);
        }
        if (rule.maxLength && value.length > rule.maxLength) {
          errors.push(`Field '${rule.field}' must be at most ${rule.maxLength} characters`);
        }
      }

      // Pattern validation
      if (rule.pattern && !rule.pattern.test(value)) {
        errors.push(`Field '${rule.field}' does not match required pattern`);
      }

      // Custom validation
      if (rule.custom && !rule.custom(value)) {
        errors.push(`Field '${rule.field}' failed custom validation`);
      }
    }

    return { valid: errors.length === 0, errors };
  }

  // Process data with integrity checks
  async processWithIntegrity<T, R>(
    operation: string,
    input: T,
    processor: (data: T) => Promise<R>,
    schema?: string
  ): Promise<{ success: boolean; result?: R; errors?: string[] }> {
    const startTime = Date.now();
    const inputHash = this.hashData(input);

    // Validate input if schema provided
    if (schema) {
      const validation = this.validate(schema, input);
      if (!validation.valid) {
        this.logProcessing({
          id: `PROC-${Date.now()}`,
          timestamp: new Date(),
          operation,
          inputHash,
          outputHash: '',
          status: 'failure',
          validationErrors: validation.errors,
          processingTime: Date.now() - startTime
        });

        return { success: false, errors: validation.errors };
      }
    }

    try {
      const result = await processor(input);
      const outputHash = this.hashData(result);
      const processingTime = Date.now() - startTime;

      this.logProcessing({
        id: `PROC-${Date.now()}`,
        timestamp: new Date(),
        operation,
        inputHash,
        outputHash,
        status: 'success',
        processingTime
      });

      this.emit('processing:success', { operation, processingTime });
      return { success: true, result };

    } catch (error) {
      const processingTime = Date.now() - startTime;

      this.logProcessing({
        id: `PROC-${Date.now()}`,
        timestamp: new Date(),
        operation,
        inputHash,
        outputHash: '',
        status: 'failure',
        validationErrors: [error instanceof Error ? error.message : 'Unknown error'],
        processingTime
      });

      this.emit('processing:error', { operation, error });
      return {
        success: false,
        errors: [error instanceof Error ? error.message : 'Unknown error']
      };
    }
  }

  private hashData(data: any): string {
    const normalized = JSON.stringify(data, Object.keys(data).sort());
    return createHash('sha256').update(normalized).digest('hex');
  }

  private logProcessing(record: ProcessingRecord): void {
    this.processingLog.push(record);

    // Retain logs for 1 year (SOC 2 requirement)
    const oneYearAgo = new Date();
    oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
    this.processingLog = this.processingLog.filter(log => log.timestamp > oneYearAgo);
  }

  // Get processing logs for audit
  getProcessingLogs(startDate?: Date, endDate?: Date): ProcessingRecord[] {
    let logs = this.processingLog;

    if (startDate) {
      logs = logs.filter(log => log.timestamp >= startDate);
    }

    if (endDate) {
      logs = logs.filter(log => log.timestamp <= endDate);
    }

    return logs;
  }

  // Calculate processing integrity metrics
  getIntegrityMetrics(period: number = 24 * 60 * 60 * 1000): {
    totalOperations: number;
    successRate: number;
    averageProcessingTime: number;
    errorRate: number;
  } {
    const cutoff = new Date(Date.now() - period);
    const recentLogs = this.processingLog.filter(log => log.timestamp > cutoff);

    if (recentLogs.length === 0) {
      return {
        totalOperations: 0,
        successRate: 100,
        averageProcessingTime: 0,
        errorRate: 0
      };
    }

    const successCount = recentLogs.filter(log => log.status === 'success').length;
    const totalTime = recentLogs.reduce((sum, log) => sum + log.processingTime, 0);

    return {
      totalOperations: recentLogs.length,
      successRate: (successCount / recentLogs.length) * 100,
      averageProcessingTime: totalTime / recentLogs.length,
      errorRate: ((recentLogs.length - successCount) / recentLogs.length) * 100
    };
  }
}

export { ProcessingIntegrityValidator, ValidationRule, ProcessingRecord };

This validator ensures:

  • Input validation against defined schemas
  • Processing integrity logging with input/output hashes
  • Error tracking and reporting
  • Performance metrics for processing operations
  • Audit trail retained for compliance requirements

For more on data quality controls, see our Data Encryption for ChatGPT Apps guide.

Confidentiality and Privacy: Encryption and Access Controls

The Confidentiality criterion requires protecting sensitive data throughout its lifecycle. For ChatGPT applications, this means encrypting data at rest and in transit, implementing proper access controls, and maintaining detailed access logs.

Encryption Service

// encryption-service.ts - SOC 2 compliant encryption
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
import { promisify } from 'util';

const scryptAsync = promisify(scrypt);

interface EncryptedData {
  encrypted: string;
  iv: string;
  authTag: string;
  algorithm: string;
  timestamp: Date;
}

interface EncryptionMetadata {
  keyId: string;
  rotationDate: Date;
  purpose: string;
}

class EncryptionService {
  private algorithm = 'aes-256-gcm';
  private keyLength = 32; // 256 bits
  private keys: Map<string, { key: Buffer; metadata: EncryptionMetadata }> = new Map();
  private currentKeyId: string;

  constructor(masterPassword: string) {
    this.initializeKeys(masterPassword);
    this.currentKeyId = 'primary';
  }

  private async initializeKeys(masterPassword: string): Promise<void> {
    // Generate primary encryption key
    const primaryKey = await scryptAsync(masterPassword, 'salt-primary', this.keyLength) as Buffer;

    this.keys.set('primary', {
      key: primaryKey,
      metadata: {
        keyId: 'primary',
        rotationDate: new Date(),
        purpose: 'General purpose encryption'
      }
    });

    // Generate separate key for PII (Personally Identifiable Information)
    const piiKey = await scryptAsync(masterPassword, 'salt-pii', this.keyLength) as Buffer;

    this.keys.set('pii', {
      key: piiKey,
      metadata: {
        keyId: 'pii',
        rotationDate: new Date(),
        purpose: 'PII encryption'
      }
    });
  }

  // Encrypt sensitive data
  async encrypt(
    plaintext: string,
    keyId: string = this.currentKeyId
  ): Promise<EncryptedData> {
    const keyData = this.keys.get(keyId);
    if (!keyData) {
      throw new Error(`Encryption key '${keyId}' not found`);
    }

    // Generate random initialization vector
    const iv = randomBytes(16);

    // Create cipher
    const cipher = createCipheriv(this.algorithm, keyData.key, iv);

    // Encrypt data
    const encrypted = Buffer.concat([
      cipher.update(plaintext, 'utf8'),
      cipher.final()
    ]);

    // Get authentication tag (for GCM mode)
    const authTag = cipher.getAuthTag();

    return {
      encrypted: encrypted.toString('hex'),
      iv: iv.toString('hex'),
      authTag: authTag.toString('hex'),
      algorithm: this.algorithm,
      timestamp: new Date()
    };
  }

  // Decrypt data
  async decrypt(
    encryptedData: EncryptedData,
    keyId: string = this.currentKeyId
  ): Promise<string> {
    const keyData = this.keys.get(keyId);
    if (!keyData) {
      throw new Error(`Decryption key '${keyId}' not found`);
    }

    // Verify algorithm matches
    if (encryptedData.algorithm !== this.algorithm) {
      throw new Error(`Algorithm mismatch: expected ${this.algorithm}, got ${encryptedData.algorithm}`);
    }

    // Create decipher
    const decipher = createDecipheriv(
      this.algorithm,
      keyData.key,
      Buffer.from(encryptedData.iv, 'hex')
    );

    // Set authentication tag
    decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));

    // Decrypt data
    const decrypted = Buffer.concat([
      decipher.update(Buffer.from(encryptedData.encrypted, 'hex')),
      decipher.final()
    ]);

    return decrypted.toString('utf8');
  }

  // Encrypt object fields selectively
  async encryptObject<T extends Record<string, any>>(
    obj: T,
    sensitiveFields: (keyof T)[],
    keyId: string = this.currentKeyId
  ): Promise<T> {
    const encrypted = { ...obj };

    for (const field of sensitiveFields) {
      if (obj[field] !== undefined && obj[field] !== null) {
        const value = typeof obj[field] === 'string'
          ? obj[field]
          : JSON.stringify(obj[field]);

        encrypted[field] = await this.encrypt(value, keyId) as any;
      }
    }

    return encrypted;
  }

  // Decrypt object fields
  async decryptObject<T extends Record<string, any>>(
    obj: T,
    sensitiveFields: (keyof T)[],
    keyId: string = this.currentKeyId
  ): Promise<T> {
    const decrypted = { ...obj };

    for (const field of sensitiveFields) {
      if (obj[field] !== undefined && obj[field] !== null) {
        const encryptedData = obj[field] as unknown as EncryptedData;
        const value = await this.decrypt(encryptedData, keyId);

        // Try to parse as JSON, fallback to string
        try {
          decrypted[field] = JSON.parse(value) as any;
        } catch {
          decrypted[field] = value as any;
        }
      }
    }

    return decrypted;
  }

  // Rotate encryption key
  async rotateKey(keyId: string, newPassword: string): Promise<void> {
    const newKey = await scryptAsync(newPassword, `salt-${keyId}-rotated`, this.keyLength) as Buffer;
    const oldKeyData = this.keys.get(keyId);

    if (!oldKeyData) {
      throw new Error(`Key '${keyId}' not found`);
    }

    this.keys.set(keyId, {
      key: newKey,
      metadata: {
        ...oldKeyData.metadata,
        rotationDate: new Date()
      }
    });
  }

  // Get key metadata for audit
  getKeyMetadata(keyId: string): EncryptionMetadata | undefined {
    return this.keys.get(keyId)?.metadata;
  }

  // Check if key rotation is needed (SOC 2 best practice: rotate annually)
  needsRotation(keyId: string): boolean {
    const metadata = this.getKeyMetadata(keyId);
    if (!metadata) return false;

    const oneYearAgo = new Date();
    oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);

    return metadata.rotationDate < oneYearAgo;
  }
}

export { EncryptionService, EncryptedData, EncryptionMetadata };

This encryption service provides:

  • AES-256-GCM encryption with authenticated encryption
  • Separate encryption keys for different data classifications (general vs PII)
  • Key rotation capabilities with metadata tracking
  • Field-level encryption for sensitive object properties
  • Audit trail for encryption operations

Access Logging and Monitoring

Implement comprehensive access logging to detect unauthorized access attempts. Your logs should capture:

  • User authentication attempts (successful and failed)
  • Resource access with timestamps and IP addresses
  • Data modifications with before/after values for critical fields
  • Administrative actions with full audit trails

For complete logging strategies, review our Security Auditing and Logging guide.

The Privacy criterion (if applicable) requires compliance with privacy regulations like GDPR. Learn more in our GDPR Compliance for ChatGPT Apps guide.

Audit Preparation: Documentation and Evidence Collection

SOC 2 Type II audits require extensive documentation and evidence collection over the 6-12 month observation period. Start preparing from day one - you can't retroactively create evidence.

Audit Evidence Collector

// audit-evidence-collector.ts - SOC 2 audit preparation
import { createWriteStream, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import { EventEmitter } from 'events';

interface EvidenceItem {
  id: string;
  timestamp: Date;
  category: 'access_control' | 'monitoring' | 'change_management' | 'incident_response' | 'backup';
  description: string;
  data: any;
  attachments?: string[];
}

interface ControlEvidence {
  controlId: string;
  controlName: string;
  testDate: Date;
  tester: string;
  result: 'pass' | 'fail' | 'partial';
  evidence: EvidenceItem[];
  notes?: string;
}

class AuditEvidenceCollector extends EventEmitter {
  private evidenceDir: string;
  private evidence: Map<string, EvidenceItem> = new Map();
  private controls: Map<string, ControlEvidence> = new Map();

  constructor(evidenceDir: string = './audit-evidence') {
    super();
    this.evidenceDir = evidenceDir;
    this.initializeDirectories();
    this.initializeControls();
  }

  private initializeDirectories(): void {
    if (!existsSync(this.evidenceDir)) {
      mkdirSync(this.evidenceDir, { recursive: true });
    }

    const subdirs = ['access-logs', 'monitoring', 'changes', 'incidents', 'backups'];
    for (const subdir of subdirs) {
      const path = join(this.evidenceDir, subdir);
      if (!existsSync(path)) {
        mkdirSync(path, { recursive: true });
      }
    }
  }

  private initializeControls(): void {
    // Define SOC 2 controls to be tested
    this.controls.set('AC-001', {
      controlId: 'AC-001',
      controlName: 'User access is restricted based on role assignments',
      testDate: new Date(),
      tester: 'System',
      result: 'pass',
      evidence: []
    });

    this.controls.set('AC-002', {
      controlId: 'AC-002',
      controlName: 'Multi-factor authentication is enforced for privileged accounts',
      testDate: new Date(),
      tester: 'System',
      result: 'pass',
      evidence: []
    });

    this.controls.set('MON-001', {
      controlId: 'MON-001',
      controlName: 'System availability is continuously monitored',
      testDate: new Date(),
      tester: 'System',
      result: 'pass',
      evidence: []
    });

    this.controls.set('CHG-001', {
      controlId: 'CHG-001',
      controlName: 'Changes to production systems are documented and approved',
      testDate: new Date(),
      tester: 'System',
      result: 'pass',
      evidence: []
    });
  }

  // Collect evidence item
  collectEvidence(item: Omit<EvidenceItem, 'id'>): string {
    const id = `EV-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const evidenceItem: EvidenceItem = { id, ...item };

    this.evidence.set(id, evidenceItem);
    this.saveEvidenceToFile(evidenceItem);
    this.emit('evidence:collected', evidenceItem);

    return id;
  }

  private saveEvidenceToFile(item: EvidenceItem): void {
    const categoryDir = this.getCategoryDirectory(item.category);
    const filename = `${item.id}.json`;
    const filepath = join(categoryDir, filename);

    const stream = createWriteStream(filepath);
    stream.write(JSON.stringify(item, null, 2));
    stream.end();
  }

  private getCategoryDirectory(category: EvidenceItem['category']): string {
    const dirMap = {
      access_control: 'access-logs',
      monitoring: 'monitoring',
      change_management: 'changes',
      incident_response: 'incidents',
      backup: 'backups'
    };

    return join(this.evidenceDir, dirMap[category]);
  }

  // Link evidence to control
  linkEvidenceToControl(evidenceId: string, controlId: string): void {
    const evidence = this.evidence.get(evidenceId);
    const control = this.controls.get(controlId);

    if (!evidence || !control) {
      throw new Error('Evidence or control not found');
    }

    control.evidence.push(evidence);
    this.emit('evidence:linked', { evidenceId, controlId });
  }

  // Test control effectiveness
  testControl(
    controlId: string,
    tester: string,
    result: ControlEvidence['result'],
    notes?: string
  ): void {
    const control = this.controls.get(controlId);
    if (!control) {
      throw new Error(`Control ${controlId} not found`);
    }

    control.testDate = new Date();
    control.tester = tester;
    control.result = result;
    control.notes = notes;

    this.emit('control:tested', control);
  }

  // Generate audit report
  generateAuditReport(startDate: Date, endDate: Date): {
    period: { start: Date; end: Date };
    controlsTested: number;
    controlsPassed: number;
    controlsFailed: number;
    evidenceCollected: number;
    summary: ControlEvidence[];
  } {
    const relevantEvidence = Array.from(this.evidence.values()).filter(
      e => e.timestamp >= startDate && e.timestamp <= endDate
    );

    const controlsPassed = Array.from(this.controls.values()).filter(
      c => c.result === 'pass'
    ).length;

    const controlsFailed = Array.from(this.controls.values()).filter(
      c => c.result === 'fail'
    ).length;

    return {
      period: { start: startDate, end: endDate },
      controlsTested: this.controls.size,
      controlsPassed,
      controlsFailed,
      evidenceCollected: relevantEvidence.length,
      summary: Array.from(this.controls.values())
    };
  }

  // Export evidence package for auditor
  exportEvidencePackage(controlIds: string[]): {
    exportDate: Date;
    controls: ControlEvidence[];
    totalEvidence: number;
  } {
    const controls = controlIds
      .map(id => this.controls.get(id))
      .filter((c): c is ControlEvidence => c !== undefined);

    const totalEvidence = controls.reduce((sum, c) => sum + c.evidence.length, 0);

    return {
      exportDate: new Date(),
      controls,
      totalEvidence
    };
  }
}

export { AuditEvidenceCollector, EvidenceItem, ControlEvidence };

Key Documentation Requirements

Your SOC 2 audit will require:

  1. System Description - Architecture diagrams, data flow maps, integration points
  2. Policies and Procedures - Information security policy, access control policy, change management procedures
  3. Risk Assessment - Annual risk assessment with identified risks and mitigation strategies
  4. Control Documentation - Detailed description of each control and how it operates
  5. Testing Evidence - Logs, screenshots, and reports demonstrating control effectiveness
  6. Incident Reports - All security incidents during observation period with resolution details
  7. Vendor Management - Due diligence documentation for third-party service providers

Selecting a Third-Party Auditor

Choose an auditor with experience in:

  • SaaS/cloud applications particularly AI-powered systems
  • Your industry (healthcare, finance, etc.) if applicable
  • OpenAI/ChatGPT integration understanding
  • Trust Service Criteria interpretation and testing

Expect the audit to take 4-8 weeks once the observation period completes. Budget $15,000-$50,000 for Type II audits depending on your system complexity.

For comprehensive security testing, review our Penetration Testing for ChatGPT Apps and Vulnerability Management guides.

Building SOC 2 Compliance Into Your ChatGPT App

SOC 2 Type II certification is not a checkbox exercise - it requires embedding security controls throughout your application architecture from the beginning. The code examples in this guide provide production-ready implementations of critical controls.

Platforms like MakeAIHQ can accelerate your SOC 2 journey by providing pre-built security controls, compliance-ready logging, and architectural patterns that align with Trust Service Criteria. When you generate ChatGPT apps through compliant platforms, you inherit SOC 2-aligned infrastructure rather than building it from scratch.

Start your SOC 2 preparation early - the 6-12 month observation period means you can't rush certification when a customer demands it. Implement controls now, collect evidence continuously, and engage auditors 3-4 months before you need the report.

Ready to build SOC 2-compliant ChatGPT applications? Start your free trial at MakeAIHQ and deploy enterprise-ready apps with built-in security controls that satisfy SOC 2 Type II requirements.


Additional Resources