Automating compliance with SOX, COSO and SCF

Compliance automation with SOX, COSO and SCF

Compliance automation with SOX, COSO and SCF
Content

In the previous post, Towards Repeatable Compliance, we explored the basics of a generic framework to define compliance requirements, extract controls and execute them as part of a deterministic verification set.

That is useful on its own, but we can do better by aligning each of the components involved with industry standards, gaining better integration and reusing existing patterns.

This post starts from SOX regulation, and introduces a well-known governance framework (COSO), a set of controls (SCF) and a standard format for expressing findings (NIST’s OSCAL).

Pipeline diagram: requirements in plain text become controls through an LLM, the controls become a SQL procedure through a second LLM, a subject-matter expert reviews the procedure and either approves it or sends it back, the approved procedure is stored in the database, and executing it marks each requirement verified or not verified.
Figure 1. 4-layers approach to implement compliance automation.

The concepts

COSO is the framework the SOX assertions are made against. Published by the Committee of Sponsoring Organizations of the Treadway Commission, it organizes internal control into five components — control environment, risk assessment, control activities, information and communication, and monitoring — expanded into 17 principles. It is a rubric, not a catalog: it says how to judge whether a set of controls is effective, but not which controls to implement.

The Secure Controls Framework is a free control catalog maintained by the SCF Council: 34 domains, more than 1,500 controls, each one written once and cross-referenced to the equivalent requirement in over 200 laws, regulations and frameworks. The crosswalks are the point. Instead of maintaining one control set for SOX, another for ISO 27001 and a third for SOC 2, SCF controls implementation allows reusability across compliance frameworks.

Finally, control results can be expressed in a standard machine-readable form. NIST’s Open Security Controls Assessment Language (OSCAL) defines a set of layered models: a catalog holds the controls, a profile selects and tailors the subset that applies to us, a component definition and a system security plan describe how they are implemented, and assessment results carry the findings. OSCAL gives the result verdicts a schema that any tool can read without knowing anything about our database.

The Role of AI

Use AI to convert SOX obligations into requirements, requirements into controls.
And nothing else.

The controls should be deterministic.

As first step, leverage AI to extract requirements from SOX, align your requirements to COSO, or define new requirements.

Then, use AI to create controls based on your data data catalogs that can be stored, audited and executed at any time with deterministic results.

Refer to Figure 1. in Towards Repeatable Compliance: an SME should be part of the loop as well.

The example: Intercompany Reconciliation

Intercompany reconciliation is one of the highest-risk areas in corporate accounting, and a standing focus of SOX 404 audits.
A parent company with subsidiaries — say a US parent, a manufacturing arm in Ireland and a distributor in Germany — has those entities constantly selling, lending and transferring among themselves.

A group cannot book profit on selling to itself, so intercompany transactions must be eliminated on consolidation.
The elimination only works if both sides recorded the same thing.
If the Irish entity books a $10M sale to the German entity and the German entity never books the purchase, the consolidated balance sheet is materially misstated: revenue never earned outside the group, and an intercompany receivable with no payable against it.

In practice three things cause the break, and none of them is fraud:

  • timing — goods shipped in December, received in January
  • currency — each side translating at its own rate
  • mismatched accounts — the same transaction booked to different GL accounts

What starts as a reconciliation difference ends as a manual year-end adjustment, and manual year-end adjustments are exactly where real misstatement hides.

From the regulation angle, it becomes an obligation. Quoting SOX §404(a):

📝Note
The annual report […] shall contain an assessment, as of the end of the most recent fiscal year of the issuer, of the effectiveness of the internal control structure and procedures of the issuer for financial reporting.

From SOX to COSO

COSO does not say “reconcile your intercompany balances”.
Instead, it determines which kind of control has to exist, and under which component a failure would be reported:

COSO componentPrincipleWhat it demands here
Information & Communication13Relevant, quality information to support internal control — both entities producing matched, comparable records
Control Activities10Control activities that mitigate risk to acceptable levels — including segregation of duties across entities
Control Activities11General control activities over technology — the ERP configuration that enforces the above

From COSO to SCF requirements

This is where the policy becomes more tangible, closer to data validation domain.

RequirementSCF control
REQ-301 — both legs recorded and reconciledORG-DCH-01 Intercompany Matching & Validation
REQ-302 — one identity cannot post both legsHRS-11 Separation of Duties
REQ-303 — every translation is reproducibleMON-03.2 Audit Trails

Notice that ORG-DCH-01 carries an ORG- prefix because intercompany matching is a financial reporting control and the SCF catalog does not contain a specific one.

We can express the requirements in EARS format:

REQ-301  IF the selling entity has not recorded a leg, OR the buying entity has not
         recorded a leg, OR either amount is absent in the reporting currency, OR the
         two amounts differ by more than 1 USD, THEN the Consolidation Service SHALL
         reject the intercompany transaction.

REQ-302  IF the identity that posted an opposing leg is not recorded, OR it is the same
         identity that posted the first leg, THEN the Consolidation Service SHALL refuse
         the posting.

REQ-303  IF a posted leg does not record the rate, OR does not record the rate source,
         OR does not record the timestamp the rate was taken at, THEN the Consolidation
         Service SHALL reject the leg.

And the same three requirements expressed in Gherkin, which provides clear evidence in case of mismatch:

Scenario: An unmatched intercompany leg blocks consolidation
  Given an intercompany reference recorded by the selling entity
  When the buying entity has not recorded the opposing leg
  Then the reference is reported as unmatched and consolidation is halted

Scenario: One identity cannot post both sides
  Given an intercompany reference with a leg in each entity
  When the two legs name the same posting identity
  Then the reference is reported as a segregation-of-duties breach

Scenario: Every translated amount can be recomputed
  Given a posted intercompany leg
  When its translation record is read
  Then it carries a rate, a source and the timestamp the rate was taken at

The experiment

We run an experiment involving 5 million trades, with a certain percentage of reconciliation breaks. Each company keeps its own ledger, in its own functional currency, and sees only its own rows.

5M
intercompany trades, 9.9M posted legs, three ledgers.
5.0%
carry a break, across four causes.
7s
to check and emit the evidence.

The generator spreads trades across the 2026 financial year and splits the 5% across the four ways an intercompany trade goes wrong:

CauseShareTradesCaught by
Only the seller records a leg2.0%100,001REQ-301
The buyer records a smaller amount — partial receipt, cut-off1.8%90,001REQ-301
One identity posts both legs0.7%35,000REQ-302
The leg arrives without rate, source or timestamp0.5%25,000REQ-303

In the following example we’ll focus on some concrete issues but you can expect the same behaviour would apply in all the reconciliation errors.

The three ledgers

The following data snippets render an idea of the data schema. Notice that each entity keeps its own ledge.

EntityNameCountryCurrencyLedger table
US-PARENTNorthwind Holdings Inc.USUSDLedgerUsParent
IE-MFGNorthwind Manufacturing Ltd.IEEURLedgerIeMfg
DE-DISTNorthwind Distribution GmbHDEEURLedgerDeDist

LedgerUsParent — a sale to Ireland, a purchase from Germany booked after the period closed:

EntryReferenceCounterpartyLegPostedAmount (local)USDPosted by
E-0001IC-1001IE-MFGAR2026-11-305M USD5MU-ALEX
E-0005IC-1003DE-DISTAP2027-01-041.44M USD1.44MU-ALEX
EG-100002AIC-1100002IE-MFGAR2026-10-1122K USD22KU-US-06
E-0006IC-1004DE-DISTAR2026-12-102M USD2MU-ALEX
EG-100052BIC-1100052IE-MFGAP2026-11-3090K USD90KU-US-15

LedgerIeMfg — the purchase from the parent, and a 10M USD sale to Germany. The EUR rows translate at 1.25, so the local and USD columns differ:

EntryReferenceCounterpartyLegPostedAmount (local)USDPosted by
E-0002IC-1001US-PARENTAP2026-11-304M EUR5MU-SIOBHAN
E-0003IC-1002DE-DISTAR2026-12-188M EUR10MU-SIOBHAN
EG-100004AIC-1100004US-PARENTAR2026-10-1324K EUR30KU-IE-06
EG-100052AIC-1100052US-PARENTAR2026-11-3072K EUR90KU-IE-13

LedgerDeDist — a sale to the parent, a purchase whose translation record lost its timestamp, and a matching entry for every trade except IC-1002:

EntryReferenceCounterpartyLegPostedAmount (local)USDPosted by
E-0004IC-1003US-PARENTAR2026-12-291.2M EUR1.5MU-KLAUS
E-0007IC-1004US-PARENTAP2026-12-101.6M EUR2MU-KLAUS
EG-100000AIC-1100000US-PARENTAR2026-10-0920K EUR25KU-DE-05
EG-100021BIC-1100021US-PARENTAP2026-10-3031K EUR39KU-DE-12

Let’s focus on four specific references. The first one is valid, the other three contain issues:

  • IC-1001 reconciles. Both ledgers posted on the same day, both translate to 5M USD, and two different identities did the posting.
  • IC-1002 raises a flag. Ireland booked the sale; Germany’s ledger holds a matching entry for every other trade and none for this one. Read E-0003 on its own terms and it is a correct posting — the break lives in the gap between two tables, so finding it takes a query that spans both.
  • IC-1003 has both legs and still breaks. Germany posted 1.2M EUR on 29 December at 1.25; the parent posted on 4 January, in the next period, at 1.20. Each entry is correct in its own book, and the two sides land 60K USD apart — which is how timing and currency differences reach a consolidation.
  • IC-1004 lacks the timestamp.

The procedures

The EARS sentences compile to invariants — exception reports, in control language. Every row returned is a transaction that consolidation would have rejected, so a healthy run returns an empty set.

The Gherkin scenarios compile to assertions that carry their own expected answer, so each reports a verdict per named case rather than a list to interpret.

EARS tests the data (ledger), Gherkin tests the validation process (checker).

Let’s go back to the reconciliation breaks:

ReferenceSellerBuyerOutcome
IC-1001US parentIrish manufacturerBoth legs, amounts agree, two identities — clean
IC-1002Irish manufacturerGerman distributorSeller books 10M USD; the buyer books nothing
IC-1003German distributorUS parentBoth legs, but posted a period apart at different rates — 60K apart
IC-1004US parentGerman distributorBoth legs agree at 2M; the buyer’s leg carries no rate timestamp

When we run the controls, REQ-301-C1 returns two exceptions, and the assessment records them:

IC-1002: no buyer leg recorded (variance 10M USD)
IC-1003: legs differ beyond tolerance (variance 60K USD)

REQ-303-C1 returns the validation error:

IC-1004: no rate timestamp recorded
EntryReferenceAmount (local)RateSourceTaken atUSD
E-0004IC-10031.2M EUR1.25ECB2026-12-29 16:001.5M
E-0007IC-10041.6M EUR1.25ECB—2M

An auditor asks two separate questions of any control:

  • whether it was designed to catch what it claims, and
  • whether it operated over the period.

REQ-301-C1 answers the second — the control ran, and it found breaks. REQ-301-C2 answers the first, and that answer is what gives an empty exception report its meaning: a check returning no rows reads the same as a check that never ran, until a scenario demonstrates it fires on the cases it should.

reqIdCOSOscfIdcontrolIdmethodologycontrolrequirement
REQ-30113ORG-DCH-01REQ-301-C1EARSFAILFAIL
REQ-30113ORG-DCH-01REQ-301-C2GherkinPASSFAIL
REQ-30210HRS-11REQ-302-C1EARSPASSPASS
REQ-30210HRS-11REQ-302-C2GherkinPASSPASS
REQ-30311MON-03.2REQ-303-C1EARSFAILFAIL
REQ-30311MON-03.2REQ-303-C2GherkinPASSFAIL

And the same run, rolled up to one row per requirement:

reqIdcosoComponentCOSOcontrolssatisfiedfailedstatus
REQ-301Information & Communication13211FAIL
REQ-302Control Activities10220PASS
REQ-303Control Activities11211FAIL

Results as OSCAL assessments

Let’s express the results following OSCAL’s assessment-results document.

An observation is what the procedure saw, a finding is what it means against the control objective. One exception, one observation. REQ-301-C1 failed on two references, so it produces two items.

Notice that all items are linked back to controls, requirement and SOX mandate.

{
  "uuid": "1dd0aa3c-…",
  "methods": ["TEST"],
  "types": ["control-objective"],
  "description": "IC-1002: no buyer leg recorded (variance 10M USD)",
  "props": [
    { "name": "control-id",     "value": "REQ-301-C1" },
    { "name": "requirement-id", "value": "REQ-301" },
    { "name": "mandate-id",     "value": "SOX-404a" }
  ],
  "links": [
    { "rel": "requirement", "href": "#b40221a3-…", "text": "REQ-301" },
    { "rel": "mandate",     "href": "#6a8f88c8-…", "text": "SOX 404(a)" }
  ],
  "collected": "2026-12-31T02:00:00Z",
  "relevant-evidence": [{ "href": "sql://procedure/check_req_301#IC-1002" }]
},
{
  "uuid": "8fe15dfb-…",
  "methods": ["TEST"],
  "types": ["control-objective"],
  "description": "IC-1003: legs differ beyond tolerance (variance 60K USD)",
  "props": [
    { "name": "control-id",     "value": "REQ-301-C1" },
    { "name": "requirement-id", "value": "REQ-301" },
    { "name": "mandate-id",     "value": "SOX-404a" }
  ],
  "links": [
    { "rel": "requirement", "href": "#b40221a3-…", "text": "REQ-301" },
    { "rel": "mandate",     "href": "#6a8f88c8-…", "text": "SOX 404(a)" }
  ],
  "collected": "2026-12-31T02:00:00Z",
  "relevant-evidence": [{ "href": "sql://procedure/check_req_301#IC-1003" }]
}

A finding stays single per control — it either holds or it does not — and names the observations it rests on.

In our example, REQ-301-C1 has two findings supporting the result:

{
  "uuid": "fe4a4553-909e-46e0-8161-75098c6f07e5",
  "title": "ORG-DCH-01 - Intercompany Matching & Validation (REQ-301-C1)",
  "props": [
    { "name": "requirement-id", "value": "REQ-301" },
    { "name": "methodology",    "value": "EARS" },
    { "name": "coso-component", "value": "Information & Communication" },
    { "name": "coso-principle", "value": "13" },
    { "name": "mandate-id",     "value": "SOX-404a" }
  ],
  "links": [
    { "rel": "requirement", "href": "#b40221a3-…", "text": "REQ-301" },
    { "rel": "mandate",     "href": "#6a8f88c8-…", "text": "SOX 404(a)" }
  ],
  "target": {
    "type": "objective-id",
    "target-id": "org-dch-01_obj",
    "status": { "state": "not-satisfied", "reason": "fail" }
  },
  "related-observations": [
    { "observation-uuid": "1dd0aa3c-2507-4e30-ac41-0dbb94d167bf" },
    { "observation-uuid": "8fe15dfb-3b84-4272-acf7-4d62ba6582b3" }
  ]
}

Traceability works both ways: a reader who starts from the IC-1002 observation navigates to REQ-301, then COSO Principle 13, then SOX 404(a). A reader who starts from SOX 404(a) obligation finds every requirement that has been defined to validate it, including controls and results.

Conclusions

Speak standards language. SOX states the obligation, COSO supplies the rubric that assertion is measured against, so a failure gets reported under a named principle instead of as a loose finding. SCF supplies the control content and the crosswalks that come with it. OSCAL turns the result into a document other systems can read.

EARS and Gherkin notation complements each other. EARS gives completeness: it compiles to an invariant that examines every row in the population and returns the ones that would have been rejected. Gherkin gives validation: named cases carrying the verdict each one is specified to produce, which proves the check fires on the breaks it claims to catch and stays quiet on a clean pair.

Compliance on correctly structured data is fast. Once a requirement is written precisely enough to compile and the data it governs is structured, the control is an ordinary query.

Unfortunately, not all data is structured. Very often, reconciliation deals with unstructured data sources as well, which is not covered in this post.

Coding Resources

As usual, sample code is available on Github.