SOX Compliance: IT Controls for Financial Reporting
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 controlsITGC Framework
| Control Domain | Objective | Key Controls |
|---|---|---|
| Access Management | Restrict access to authorised users | User provisioning, access reviews, privileged access |
| Change Management | Ensure authorised changes only | Change approval, testing, segregation of duties |
| Operations | Ensure reliable processing | Job scheduling, backup/recovery, incident management |
| Program Development | Ensure secure, tested systems | SDLC 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 recordsCI/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 yearsChange 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 reportsBackup 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
-
Document everything: SOX requires demonstrable evidence of control operation
-
Automate where possible: Automated controls are more reliable and easier to test
-
Segregation of duties: Critical for preventing fraud—enforce in systems and processes
-
Retain evidence: 7-year retention requirement for all SOX-related documentation
-
Regular testing: Don't wait for auditors—test controls quarterly
-
Access reviews matter: Quarterly access reviews are a fundamental ITGC requirement
-
Change control rigour: Every production change needs approval, testing, and documentation
-
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.