Implement Sarbanes-Oxley (SOX) controls in your engineering workflow. Covers change management, access controls, audit trails, separation of duties, and automated compliance evidence.
Engineering teams must ensure SOX compliance in their systems by demonstrating the integrity, accuracy, and auditability of financial data and processes. This is most critical during annual financial audits, when the internal control over financial reporting (ICFR) must be validated across systems used for revenue recognition, accounts payable, general ledger, and payroll. The core requirement is that every financial transaction, from entry to reporting, must be traceable, verifiable, and resistant to tampering. This page details the actions, configurations, and checks that engineering teams perform to meet SOX’s internal control standards.
Ensure every financial data pipeline includes a source, transformation, and target system with explicit audit trails. The workflow must be idempotent, versioned, and validated at each step.
A typical pipeline for revenue recognition begins with a POST /api/v1/transactions call. The payload is validated against a JSON Schema:
{
"type": "object",
"required": ["invoice_id", "amount", "customer_id", "date"],
"properties": {
"invoice_id": { "type": "string", "pattern": "^INV-\\d{6}$" },
"amount": { "type": "number", "minimum": 0.01 },
"customer_id": { "type": "string" },
"date": { "type": "string", "format": "date" }
}
}
When the API receives a transaction, it stores the raw event in a raw_transactions table and generates a processed_transactions record with a processed_at timestamp and validated_by ID.
CREATE TABLE raw_transactions (
id SERIAL PRIMARY KEY,
payload JSONB NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
validated_at TIMESTAMPTZ,
processed_at TIMESTAMPTZ,
error TEXT
);
CREATE TABLE processed_transactions (
id SERIAL PRIMARY KEY,
invoice_id VARCHAR(20) UNIQUE NOT NULL,
amount DECIMAL(12,2) NOT NULL,
customer_id VARCHAR(50) NOT NULL,
date DATE NOT NULL,
processed_at TIMESTAMPTZ NOT NULL,
source_system VARCHAR(50) NOT NULL,
batch_id VARCHAR(50) NOT NULL,
audit_hash BYTEA NOT NULL
);
The audit_hash column stores a SHA-256 digest of the entire input payload, computed before any transformation. This digest is recalculated after processing and stored to enable change detection.
$ echo '{"invoice_id":"INV-001234","amount":1999.99,"customer_id":"CUST-8877","date":"2024-03-15"}' | \
openssl dgst -sha256 -binary | \
xxd -p
b0b8d5a1f3c8e2b6d9a5c1e7b3d8f2a6c1e8d4b5a7c3b1e9d6c2f4b8a1e5d3c7
The audit hash is stored in processed_transactions.audit_hash and used to detect any drift between source and processed data.
Every processing job must be idempotent and idempotency is verified via a batch_id and run_id. A batch process reads from raw_transactions, applies transformations, and writes to processed_transactions with a unique batch_id and run_id.
$ ./process-revenue-transactions \
--batch-id "batch-2024-03-15-01" \
--run-id "run-2024-03-15-01-2345" \
--source-dsn "postgresql://prod:5432/finance" \
--target-dsn "postgresql://analytics:5432/finance"
The process logs the following key metrics:
input_count: number of raw records readoutput_count: number of processed records writtenduplicate_count: number of records already in processed_transactionserror_count: number of failed recordsrun_duration: total time in secondsAfter processing, the job updates the raw_transactions table with validated_at and processed_at timestamps, and inserts a batch_metadata record:
INSERT INTO batch_metadata (
batch_id,
run_id,
input_count,
output_count,
duplicate_count,
error_count,
run_duration,
started_at,
completed_at,
audit_hash
) VALUES (
'batch-2024-03-15-01',
'run-2024-03-15-01-2345',
1243,
1241,
2,
0,
47.32,
'2024-03-15T23:45:00Z',
'2024-03-15T23:45:47Z',
'b0b8d5a1f3c8e2b6d9a5c1e7b3d8f2a6c1e8d4b5a7c3b1e9d6c2f4b8a1e5d3c7'
);
The audit_hash for the batch is computed from the hash of all batch_metadata rows and the processed_transactions records.
Every deployment must be traceable, tested, and approved. The CI/CD pipeline enforces the following SOX controls:
The pipeline uses git tags to mark releases and systemctl to manage services. A release is triggered by a git tag:
$ git tag -a v1.2.3 -m "Revenue recognition improvements"
$ git push origin v1.2.3
The CI job runs in a container with docker-compose and executes the following steps:
docker build --tag myapp:v1.2.3 .docker run --rm myapp:v1.2.3 /usr/bin/health-checksystemctl on the target host:$ sudo systemctl stop revenue-api
$ sudo systemctl start revenue-api
$ sudo systemctl status revenue-api --no-pager --full
Deployment logs must include:
start_time: when the systemctl start command beganend_time: when the service reported readydeployment_id: unique identifier for the deploymentchanges: list of configuration and code changes appliedThe deployment script logs to /var/log/deployments.log with the following format:
2024-03-15T23:45:00Z [DEPLOY] deployment_id=20240315-01, service=revenue-api, version=v1.2.3, status=success, changes=[
"config/database.yml: added connection pooling",
"src/revenue.py: fixed rounding in invoice totals"
]
The deployment_id is recorded in deployments table:
CREATE TABLE deployments (
id SERIAL PRIMARY KEY,
deployment_id VARCHAR(50) UNIQUE NOT NULL,
service_name VARCHAR(100) NOT NULL,
version VARCHAR(50) NOT NULL,
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ,
status VARCHAR(20) NOT NULL CHECK (status IN ('success', 'failed', 'partial')),
changes JSONB NOT NULL,
logs TEXT,
audit_hash BYTEA NOT NULL
);
The audit_hash is computed from all fields in the deployments record and stored for future verification.
Every code change must be reviewed and approved via pull requests. The PR must include:
The PR is merged only when all checks pass: unit tests, integration tests, and a security scan.
The merge commit includes a ChangeLog entry:
## v1.2.3 (2024-03-15)
### Features
- Add invoice-level tax calculation (CUST-8877)
### Bug Fixes
- Fix rounding error in invoice total (CUST-8877)
### Changes
- Updated `database.yml` to use connection pooling
- Added `audit_hash` to `processed_transactions`
The ChangeLog is stored in CHANGELOG.md and versioned with git.
Automated scanning ensures that systems remain compliant over time. Scans run daily and validate:
A scanning job runs via cron every 24 hours:
0 2 * * * /opt/scans/sox-validate.sh
The script performs the following checks:
#!/bin/bash
set -euo pipefail
# Validate that every raw transaction has a corresponding processed record
psql -t -A -F ',' -c "
SELECT
COUNT(*) AS missing,
SUM(1) FILTER (WHERE p.id IS NULL) AS count_raw,
AVG(EXTRACT(epoch FROM (p.processed_at - r.received_at))) AS avg_lag
FROM raw_transactions r
LEFT JOIN processed_transactions p ON r.invoice_id = p.invoice_id
WHERE r.validated_at IS NOT NULL
" > /var/log/sox/data-integrity.csv
# Check audit hash consistency
psql -t -A -F ',' -c "
SELECT
COUNT(*) AS mismatched,
COUNT(*) FILTER (WHERE r.audit_hash <> p.audit_hash) AS hash_mismatch
FROM raw_transactions r
JOIN processed_transactions p ON r.invoice_id = p.invoice_id
WHERE r.validated_at IS NOT NULL
" >> /var/log/sox/data-integrity.csv
The output includes:
missing: number of raw records not processedcount_raw: total number of validated raw recordsavg_lag: average time between receipt and processingmismatched: number of records with mismatched audit hasheshash_mismatch: number of records where hash differs between raw and processedA mismatched audit hash indicates that data was altered during processing.
The scan checks that all key controls are active:
Each control is defined in a controls.json file:
[
{
"control_id": "GL-REVENUE-RECONCILIATION",
"description": "Reconcile revenue data with general ledger at end of day",
"frequency": "daily",
"expected": "all records in revenue table exist in GL table",
"last_run": "2024-03-15T00:00:00Z",
"status": "success",
"result": {
"total_records": 1241,
"matched": 1241,
"missing": 0,
"last_updated": "2024-03-15T00:05:12Z"
}
},
{
"control_id": "LARGE-TRANSACTION-APPROVAL",
"description": "All transactions over $10,000 require approval",
"frequency": "continuous",
"expected": "every invoice with amount > 10000 has an approval record",
"last_run": "2024-03-15T00:00:00Z",
"status": "failed",
"result": {
"total_invoices": 1241,
"approved": 1225,
"missing_approval": 16,
"last_updated": "2024-03-15T00:04:33Z"
}
}
]
The scan compares the current state with the expected state and logs any deviations.
The scan verifies that backups are taken and stored:
# Check that daily backup exists and is complete
$ ls /backups/finance/2024-03-15/
finance-db-dump.sql.gz
finance-db-dump.sql.gz.sha256
finance-db-dump.sql.gz.md5
# Verify checksums
$ sha256sum -c /backups/finance/2024-03-15/finance-db-dump.sql.gz.sha256
finance-db-dump.sql.gz: OK
$ md5sum -c /backups/finance/2024-03-15/finance-db-dump.sql.gz.md5
finance-db-dump.sql.gz: OK
If any backup is missing or checksums fail, the scan logs an alert.
Every action in a financial system must be logged with sufficient detail to allow an auditor to reconstruct the full history. The audit trail includes:
The audit trail is maintained in audit_log table:
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
user_id VARCHAR(100) NOT NULL,
source_system VARCHAR(100) NOT NULL,
target_table VARCHAR(100) NOT NULL,
record_id VARCHAR(100),
old_values JSONB,
new_values JSONB,
action_notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
verification_status VARCHAR(20) DEFAULT 'pending',
verified_by VARCHAR(100),
verified_at TIMESTAMPTZ,
audit_hash BYTEA NOT NULL
);
An example entry for a new invoice:
INSERT INTO audit_log (
event_type,
user_id,
source_system,
target_table,
record_id,
old_values,
new_values,
action_notes,
created_at,
verification_status,
verified_by
) VALUES (
'CREATE',
'[email protected]',
'web-app',
'processed_transactions',
'INV-001234',
'{}',
'{
"invoice_id": "INV-001234",
"amount": 1999.99,
"customer_id": "CUST-8877",
"date": "2024-03-15"
}',
'Invoice created from web form',
'2024-03-15T23:45:00Z',
'verified',
'[email protected]'
);
The audit_hash is computed from all fields and stored for future validation.
When an auditor requests documentation, the team provides:
The team responds to a finding with a Corrective Action Report (CAR), which includes:
A CAR for a failed backup might include:
rsync logls -l and sha256sumcronsystemctl status backup-jobAll evidence is versioned and timestamped, and the CAR is signed with a digital signature using gpg.
The team maintains a SOX Compliance Log, a single file that tracks all compliance activities:
date,activity,system,owner,notes,verified,verification_date
2024-03-15,run automated scan,finance-system,[email protected],"Data integrity check passed",yes,2024-03-15
2024-03-15,deploy v1.2.3,revenue-api,[email protected],"Rollback tested",yes,2024-03-15
2024-03-15,validate backup,finance-db,backup-service,"Backup missing checksums",no,2024-03-16
This log is reviewed quarterly and updated after every audit.
received_at and processed_at timestamps use different time zones, causing hash mismatches even when data is identical.run_id in batch processing: when run_id is not recorded, the batch cannot be traced back to a specific deployment.audit_hash computation: the hash is computed over JSON without sorting keys, leading to inconsistent results.systemctl deployment: systemctl start returns before the service is ready, causing run_duration to be too short.sha256 and md5 files are not updated after new backups are created.These failures are often silent: the system appears to work, but the audit trail is incomplete and inconsistent.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy