Skip to main content
Back to Blog
25 October 202515 min read

SOX Compliance: IT Controls for Financial Reporting

ComplianceSOXSecurityEnterpriseFinance

Implementing IT General Controls (ITGCs) for Sarbanes-Oxley compliance. Access management, change control, and automated compliance in modern tech environments.


SOX Compliance: IT Controls for Financial Reporting

The Sarbanes-Oxley Act (SOX) requires publicly traded companies to maintain effective internal controls over financial reporting. For technology teams, this means implementing robust IT General Controls (ITGCs) that auditors can test and verify.

Understanding SOX IT Requirements

SOX Section 404 Overview

SOX Section 404 Requirements:
├── Management Responsibilities
│   ├── Design and maintain internal controls
│   ├── Assess control effectiveness annually
│   ├── Document control framework
│   └── Report on control effectiveness
│
├── Auditor Responsibilities
│   ├── Audit internal control assessment
│   ├── Test control operating effectiveness
│   ├── Report on material weaknesses
│   └── Issue opinion on controls
│
└── IT Control Categories
    ├── IT General Controls (ITGCs)
    │   ├── Access to programs and data
    │   ├── Program change management
    │   ├── Program development
    │   └── Computer operations
    │
    └── Application Controls
        ├── Input controls
        ├── Processing controls
        └── Output controls

ITGC Framework

Control DomainObjectiveKey Controls
Access ManagementRestrict access to authorised usersUser provisioning, access reviews, privileged access
Change ManagementEnsure authorised changes onlyChange approval, testing, segregation of duties
OperationsEnsure reliable processingJob scheduling, backup/recovery, incident management
Program DevelopmentEnsure secure, tested systemsSDLC controls, testing requirements, go-live approval

Access to Programs and Data

User Access Management

// sox-access-controls.ts interface UserAccessControl { controlId: string; objective: string; controlActivities: ControlActivity[]; testingProcedures: TestProcedure[]; } interface ControlActivity { activity: string; frequency: 'per_occurrence' | 'daily' | 'weekly' | 'monthly' | 'quarterly'; evidence: string[]; owner: string; } const accessManagementControls: UserAccessControl[] = [ { controlId: 'ITGC-ACC-001', objective: 'New user access is appropriately authorised', controlActivities: [ { activity: 'Access requests require manager approval before provisioning', frequency: 'per_occurrence', evidence: [ 'Access request tickets', 'Manager approval records', 'Provisioning logs' ], owner: 'IT Service Desk Manager' } ], testingProcedures: [ { procedure: 'Select sample of new users provisioned during period', sampleSize: 25, testSteps: [ 'Verify access request exists', 'Confirm manager approval obtained before access granted', 'Validate access matches approved request', 'Ensure onboarding completed timely' ] } ] }, { controlId: 'ITGC-ACC-002', objective: 'Terminated user access is removed timely', controlActivities: [ { activity: 'User access disabled within 24 hours of termination', frequency: 'per_occurrence', evidence: [ 'Termination notification from HR', 'Access revocation ticket', 'System access logs showing disable time' ], owner: 'IT Security Manager' } ], testingProcedures: [ { procedure: 'Select sample of terminated employees during period', sampleSize: 25, testSteps: [ 'Obtain termination date from HR records', 'Verify access disabled within 24 hours', 'Confirm all system access removed', 'Test that terminated user cannot authenticate' ] } ] }, { controlId: 'ITGC-ACC-003', objective: 'User access is periodically reviewed and recertified', controlActivities: [ { activity: 'Quarterly access review by system owners', frequency: 'quarterly', evidence: [ 'Access review reports', 'System owner sign-off', 'Remediation records for removed access' ], owner: 'System Owners' } ], testingProcedures: [ { procedure: 'Verify quarterly access reviews completed', testSteps: [ 'Confirm review performed for all in-scope systems', 'Verify system owner performed review', 'Validate remediation of identified issues', 'Confirm timeliness of review completion' ] } ] } ];

Privileged Access Controls

// privileged-access.ts interface PrivilegedAccessControl { controlId: string; privilegedRoles: string[]; controlMeasures: ControlMeasure[]; monitoringRequirements: MonitoringRequirement[]; } const privilegedAccessFramework: PrivilegedAccessControl = { controlId: 'ITGC-ACC-004', privilegedRoles: [ 'Database Administrator', 'System Administrator', 'Network Administrator', 'Security Administrator', 'Application Administrator' ], controlMeasures: [ { measure: 'Privileged accounts require additional approval', implementation: ` Privileged access requests require approval from: 1. Direct manager 2. System owner 3. IT Security `, evidence: ['Multi-level approval workflow records'] }, { measure: 'Privileged access is time-limited', implementation: ` Administrative access granted for maximum 90 days. Renewal requires re-approval process. `, evidence: ['Access expiration dates', 'Renewal requests'] }, { measure: 'Privileged sessions are monitored and recorded', implementation: ` All privileged sessions logged via PAM solution. Session recordings retained for 1 year. `, evidence: ['Session logs', 'PAM reports'] }, { measure: 'Shared accounts are prohibited for privileged access', implementation: ` Each administrator has individual named account. Service accounts are documented with designated owners. `, evidence: ['Account inventory', 'Service account registry'] } ], monitoringRequirements: [ { requirement: 'Review privileged account activity monthly', metric: 'Privileged commands executed', alertThreshold: 'Unusual activity patterns' }, { requirement: 'Review privileged account inventory quarterly', metric: 'Number of privileged accounts vs baseline', alertThreshold: '10% increase from prior quarter' } ] }; // Automated access review implementation const performAccessReview = async ( systemId: string, reviewPeriod: string ): Promise<AccessReviewResult> => { // Get current access list const currentAccess = await getSystemAccess(systemId); // Get active employees from HR const activeEmployees = await hrSystem.getActiveEmployees(); // Get approved access from access management system const approvedAccess = await accessManagement.getApprovedAccess(systemId); // Identify discrepancies const findings: AccessReviewFinding[] = []; for (const access of currentAccess) { // Check if user is still active if (!activeEmployees.find(e => e.id === access.userId)) { findings.push({ type: 'terminated_user_with_access', severity: 'high', userId: access.userId, accessLevel: access.level }); } // Check if access is approved if (!approvedAccess.find(a => a.userId === access.userId && a.level === access.level )) { findings.push({ type: 'unapproved_access', severity: 'medium', userId: access.userId, accessLevel: access.level }); } } return { systemId, reviewPeriod, totalUsers: currentAccess.length, findings, requiresRemediation: findings.length > 0, reviewDate: new Date() }; };

Program Change Management

Change Control Framework

# change-management-controls.yml change_control_framework: ITGC-CHG-001: objective: Changes are authorised before implementation control_activities: - All changes require documented change request - Business owner approval for business changes - Technical approval for infrastructure changes - CAB approval for high-risk changes evidence: - Change request tickets - Approval records - CAB meeting minutes ITGC-CHG-002: objective: Changes are tested before production deployment control_activities: - Unit testing completed by developers - Integration testing in test environment - User acceptance testing for business changes - Performance testing for significant changes evidence: - Test plans and results - UAT sign-off - Test environment logs ITGC-CHG-003: objective: Segregation of duties between development and deployment control_activities: - Developers cannot deploy to production - Separate teams for development and operations - Code review required before merge - Production access restricted to operations team evidence: - Access control matrix - Deployment logs showing deployer - Code review records ITGC-CHG-004: objective: Emergency changes follow expedited approval process control_activities: - Emergency changes require verbal approval - Documented within 24 hours - Retroactive CAB review required - Root cause analysis completed evidence: - Emergency change tickets - After-the-fact documentation - CAB review records

CI/CD Pipeline Controls

# .github/workflows/sox-compliant-deployment.yml name: SOX-Compliant Deployment Pipeline on: push: branches: [main] env: REQUIRE_APPROVALS: true MIN_REVIEWERS: 2 jobs: # ITGC-CHG-002: Testing controls automated-testing: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run unit tests run: npm test -- --coverage - name: Run integration tests run: npm run test:integration - name: Upload test results uses: actions/upload-artifact@v4 with: name: test-results-${{ github.sha }} path: | coverage/ test-results/ retention-days: 2555 # 7 years for SOX - name: Verify test coverage threshold run: | COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct') if (( $(echo "$COVERAGE < 80" | bc -l) )); then echo "Coverage $COVERAGE% below 80% threshold" exit 1 fi # ITGC-CHG-003: Segregation of duties verification verify-segregation: runs-on: ubuntu-latest needs: automated-testing steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Verify code review completed run: | PR_NUMBER=$(gh pr list --state merged --json number --jq '.[0].number') REVIEWS=$(gh pr view $PR_NUMBER --json reviews --jq '.reviews | length') if [[ $REVIEWS -lt ${{ env.MIN_REVIEWERS }} ]]; then echo "Insufficient code reviews: $REVIEWS < ${{ env.MIN_REVIEWERS }}" exit 1 fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Verify author is not approver run: | PR_NUMBER=$(gh pr list --state merged --json number --jq '.[0].number') AUTHOR=$(gh pr view $PR_NUMBER --json author --jq '.author.login') APPROVERS=$(gh pr view $PR_NUMBER --json reviews --jq '.reviews[].author.login') if echo "$APPROVERS" | grep -q "$AUTHOR"; then echo "Author cannot approve own changes" exit 1 fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ITGC-CHG-001: Change authorisation require-approval: runs-on: ubuntu-latest needs: verify-segregation environment: production steps: - name: Await deployment approval run: echo "Deployment approved by authorized approver" # Deployment with audit trail deploy-production: runs-on: ubuntu-latest needs: require-approval steps: - uses: actions/checkout@v4 - name: Generate change record id: change-record run: | echo "CHANGE_ID=CHG-$(date +%Y%m%d)-${{ github.run_number }}" >> $GITHUB_OUTPUT echo "DEPLOYER=${{ github.actor }}" >> $GITHUB_OUTPUT echo "TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT - name: Deploy to production run: | npm run deploy:production env: CHANGE_ID: ${{ steps.change-record.outputs.CHANGE_ID }} - name: Create audit record run: | cat << EOF > audit-record.json { "changeId": "${{ steps.change-record.outputs.CHANGE_ID }}", "commitSha": "${{ github.sha }}", "deployer": "${{ steps.change-record.outputs.DEPLOYER }}", "timestamp": "${{ steps.change-record.outputs.TIMESTAMP }}", "environment": "production", "approvers": "${{ github.event.deployment.creator.login }}", "testResults": "test-results-${{ github.sha }}" } EOF - name: Upload audit record uses: actions/upload-artifact@v4 with: name: audit-${{ steps.change-record.outputs.CHANGE_ID }} path: audit-record.json retention-days: 2555 # 7 years

Change Evidence Collection

// change-evidence-collector.ts interface ChangeRecord { changeId: string; requestDate: Date; requestor: string; description: string; riskLevel: 'low' | 'medium' | 'high' | 'critical'; approvals: Approval[]; testing: TestingEvidence; deployment: DeploymentRecord; postImplementationReview?: PIRRecord; } interface TestingEvidence { unitTestResults: TestResult; integrationTestResults: TestResult; uatSignOff?: { signedBy: string; signedDate: Date; comments: string; }; performanceTestResults?: TestResult; } const collectChangeEvidence = async ( changeId: string ): Promise<ChangeEvidencePackage> => { const change = await changeManagement.getChange(changeId); // Collect approval evidence const approvalEvidence = await collectApprovalEvidence(changeId); // Collect testing evidence const testingEvidence = await collectTestingEvidence(change.commitSha); // Collect deployment evidence const deploymentEvidence = await collectDeploymentEvidence(changeId); // Collect segregation of duties evidence const sodEvidence = await verifySoD(change); return { changeId, collectionDate: new Date(), change, evidence: { approvals: approvalEvidence, testing: testingEvidence, deployment: deploymentEvidence, segregationOfDuties: sodEvidence }, compliant: evaluateCompliance({ approvalEvidence, testingEvidence, deploymentEvidence, sodEvidence }) }; }; const verifySoD = async (change: ChangeRecord): Promise<SoDEvidence> => { const developer = change.requestor; const reviewers = await getCodeReviewers(change.commitSha); const deployer = change.deployment.deployedBy; const violations: string[] = []; // Developer cannot be reviewer if (reviewers.includes(developer)) { violations.push('Developer approved own code'); } // Developer cannot be deployer if (deployer === developer) { violations.push('Developer deployed own code'); } return { developer, reviewers, deployer, violations, compliant: violations.length === 0 }; };

Computer Operations Controls

Batch Processing and Job Scheduling

# operations-controls.yml ITGC-OPS-001: objective: Batch jobs are scheduled and monitored control_activities: - Critical batch jobs have defined schedules - Job failures generate alerts - Failed jobs are investigated and resolved - Job completion verified before dependent processes evidence: - Job schedules - Monitoring dashboards - Alert logs - Incident tickets for failures ITGC-OPS-002: objective: Data backups are performed and tested control_activities: - Daily backups of financial systems - Weekly verification of backup integrity - Annual recovery testing - Off-site backup storage evidence: - Backup logs - Integrity verification reports - Recovery test results - Off-site storage receipts ITGC-OPS-003: objective: Incidents are tracked and resolved control_activities: - All incidents logged in ticketing system - Severity-based escalation procedures - Root cause analysis for significant incidents - Management review of incident trends evidence: - Incident tickets - Escalation records - RCA reports - Monthly incident reports

Backup and Recovery Controls

// backup-controls.ts interface BackupControl { systemId: string; backupSchedule: BackupSchedule; retentionPolicy: RetentionPolicy; recoveryObjectives: RecoveryObjectives; testingRequirements: TestingRequirements; } interface BackupSchedule { frequency: 'hourly' | 'daily' | 'weekly'; time: string; type: 'full' | 'incremental' | 'differential'; destination: 'local' | 'remote' | 'both'; } interface RecoveryObjectives { rto: number; // Recovery Time Objective in hours rpo: number; // Recovery Point Objective in hours } const financialSystemBackups: BackupControl[] = [ { systemId: 'ERP_PRODUCTION', backupSchedule: { frequency: 'daily', time: '02:00', type: 'full', destination: 'both' }, retentionPolicy: { daily: 30, weekly: 52, monthly: 84, // 7 years for SOX yearly: 7 }, recoveryObjectives: { rto: 4, rpo: 24 }, testingRequirements: { recoveryTestFrequency: 'quarterly', integrityCheckFrequency: 'weekly', lastRecoveryTest: new Date('2025-01-15'), lastIntegrityCheck: new Date('2025-01-20') } } ]; const performBackupIntegrityCheck = async ( systemId: string ): Promise<IntegrityCheckResult> => { const latestBackup = await getLatestBackup(systemId); // Verify backup completeness const completenessCheck = await verifyBackupCompleteness(latestBackup); // Verify backup integrity (checksum validation) const integrityCheck = await verifyBackupIntegrity(latestBackup); // Verify backup is restorable (test restore to isolated environment) const restorabilityCheck = await testRestore(latestBackup, 'integrity_test'); const result: IntegrityCheckResult = { systemId, backupId: latestBackup.id, checkDate: new Date(), completeness: completenessCheck, integrity: integrityCheck, restorability: restorabilityCheck, overallResult: completenessCheck.passed && integrityCheck.passed && restorabilityCheck.passed }; // Log result for SOX evidence await logBackupCheck(result); return result; };

Audit Evidence and Documentation

Control Documentation Template

// control-documentation.ts interface ControlDocumentation { controlId: string; controlObjective: string; riskAddressed: string; controlOwner: string; controlFrequency: string; controlDescription: string; keyControlIndicators: string[]; testingApproach: TestingApproach; evidenceRetention: EvidenceRetention; } interface TestingApproach { testType: 'inquiry' | 'observation' | 'inspection' | 'reperformance'; sampleSize: number | 'all'; testingPeriod: string; testProcedures: string[]; } interface EvidenceRetention { retentionPeriod: string; storageLocation: string; accessControls: string; } const soxControlDocumentation: ControlDocumentation = { controlId: 'ITGC-ACC-001', controlObjective: 'New user access is appropriately authorised before provisioning', riskAddressed: 'Unauthorised access to financial systems could lead to fraudulent transactions or data manipulation', controlOwner: 'IT Service Desk Manager', controlFrequency: 'Per occurrence (each new user request)', controlDescription: ` When a new employee requires access to financial systems: 1. Employee's manager submits access request via ServiceNow 2. Request routed to system owner for approval 3. Upon approval, IT provisions access per approved request 4. Confirmation sent to requester and approvers 5. Access logged in central access management system `, keyControlIndicators: [ 'All access requests have documented manager approval', 'Access provisioned matches approved request', 'No access provisioned without prior approval', 'Provisioning completed within SLA (24 hours)' ], testingApproach: { testType: 'inspection', sampleSize: 25, testingPeriod: 'Quarterly', testProcedures: [ 'Select random sample of new users from provisioning log', 'Obtain access request ticket for each user', 'Verify manager approval exists and predates provisioning', 'Compare approved access to actual provisioned access', 'Document any exceptions identified' ] }, evidenceRetention: { retentionPeriod: '7 years', storageLocation: 'SharePoint SOX Evidence Library', accessControls: 'Restricted to SOX team and external auditors' } };

Automated Evidence Collection

// sox-evidence-automation.ts interface SOXEvidenceCollector { scheduleCollection(): void; collectAccessEvidence(): Promise<AccessEvidence>; collectChangeEvidence(): Promise<ChangeEvidence>; collectOperationsEvidence(): Promise<OperationsEvidence>; generateAuditPackage(period: string): Promise<AuditPackage>; } const collectQuarterlyEvidence = async ( quarter: string ): Promise<QuarterlyEvidencePackage> => { const [accessEvidence, changeEvidence, opsEvidence] = await Promise.all([ collectAccessManagementEvidence(quarter), collectChangeManagementEvidence(quarter), collectOperationsEvidence(quarter) ]); return { period: quarter, collectionDate: new Date(), evidence: { accessManagement: { newUserRequests: accessEvidence.newUsers, terminationProcessing: accessEvidence.terminations, accessReviews: accessEvidence.reviews, privilegedAccessLogs: accessEvidence.privilegedAccess }, changeManagement: { changeRequests: changeEvidence.changes, testingRecords: changeEvidence.testing, deploymentLogs: changeEvidence.deployments, emergencyChanges: changeEvidence.emergencies }, operations: { backupLogs: opsEvidence.backups, recoveryTests: opsEvidence.recoveryTests, incidentTickets: opsEvidence.incidents, jobScheduleLogs: opsEvidence.batchJobs } }, controlTestResults: await runAutomatedControlTests(quarter), exceptionsIdentified: await identifyExceptions(quarter) }; }; const generateAuditorReport = async ( period: string ): Promise<AuditorReport> => { const evidence = await collectQuarterlyEvidence(period); return { period, generatedDate: new Date(), preparedBy: 'IT SOX Compliance Team', executiveSummary: generateExecutiveSummary(evidence), controlMatrix: generateControlMatrix(evidence), testingResults: { controlsTested: evidence.controlTestResults.length, controlsPassed: evidence.controlTestResults.filter(r => r.passed).length, exceptionsIdentified: evidence.exceptionsIdentified.length, remediationStatus: await getRemediationStatus(evidence.exceptionsIdentified) }, evidenceIndex: generateEvidenceIndex(evidence), appendices: { sampleSelections: evidence.controlTestResults.map(r => r.sample), exceptionDetails: evidence.exceptionsIdentified, remediationPlans: await getRemediationPlans(evidence.exceptionsIdentified) } }; };

Key Takeaways

  1. Document everything: SOX requires demonstrable evidence of control operation

  2. Automate where possible: Automated controls are more reliable and easier to test

  3. Segregation of duties: Critical for preventing fraud—enforce in systems and processes

  4. Retain evidence: 7-year retention requirement for all SOX-related documentation

  5. Regular testing: Don't wait for auditors—test controls quarterly

  6. Access reviews matter: Quarterly access reviews are a fundamental ITGC requirement

  7. Change control rigour: Every production change needs approval, testing, and documentation

  8. Integrate with DevOps: Modern CI/CD can enforce SOX controls automatically

SOX compliance isn't just about passing audits—it's about establishing controls that protect the integrity of financial reporting. Well-designed ITGCs provide assurance that financial data is accurate and systems are reliable.

Share this article