Building compliant software in financial services / Auditing what you have
The compliance gaps teams miss in their own app
In a recent engagement with a financial analytics firm, we audited an application the team believed was compliant. It was not, in several ways nobody there knew about. None of it was carelessness. Every gap sat in the part of the system nobody had designed, which is exactly where these gaps are.
Published August 22, 2026. Editorial.
Key takeaways
- Self-review compares the system against its own design, and compliance gaps are almost never in the designed part.
- The recurring patterns are read logging, retention enforced in the wrong layer, identity in the audit trail, and copies of data outside the controlled path.
- A gap that produces no user-visible symptom will persist indefinitely, because nothing creates pressure to find it.
- The fix starts with writing the obligations down, because you cannot review against a specification that does not exist.
In a recent engagement with a financial analytics firm, we were asked to look at an existing application ahead of a broader piece of work. The team was competent, the product was in production and working, and the expectation on both sides was that we would find some technical debt and move on.
What we found instead was a set of compliance gaps that nobody at the company knew about. We are describing them here without naming the client, without detail that would identify them, and without any claim about how common this is, because one engagement is not a statistic. What the engagement did was make clearer a pattern we already believed in, and the pattern is the useful part.
Not one of the gaps was carelessness. Every one sat in a part of the system nobody had designed.
Why the design is never where the gap is
When a team reviews its own application for compliance, the review is against the design. Someone opens the architecture, goes through the flows, checks the controls they built, and confirms that the controls they built are present. They are. That is why the review finds nothing.
Compliance gaps are not there. They are in the space between the things that were designed: the path added later that did not go through the component carrying the control, the copy of the data created for a reason unrelated to the control, the case the original design did not contemplate because at the time it did not exist.
That is a structural problem, not an attention problem. You cannot review against a specification you never wrote down, and the reason these gaps exist in the first place is that the obligation was never written down as a system requirement. It was read, understood, implemented approximately, and then summarised into a policy. The policy describes the intent. Nothing describes what the system must do, so nothing can be checked against.
The second structural problem is that none of it produces a symptom. A missing read log breaks nothing. Nobody files a ticket. Support does not escalate it. Revenue does not move. A defect with no symptom survives indefinitely, because every mechanism a team has for finding problems is driven by something going visibly wrong.
The patterns that recur
These are the ones worth checking first in any financial application that has been running for more than a couple of years.
Logging built for writes. The audit system was designed to answer "what changed," because that is the question product teams have. The regulatory question is closer to "who saw this," and the answer is often nowhere. The FTC Safeguards Rule requirement to monitor and log the activity of authorized users [1] is not satisfied by a change history, and the pattern the provision exists to catch, someone with legitimate access reading far more than their job requires, generates no writes at all.
Retention enforced in the interface. The delete button deletes. The retention job runs on the primary table. Both work. Meanwhile the analytics replica has its own lifecycle, the nightly export lands in object storage with a bucket policy somebody set years ago, the backup tier has a provider default, and the log aggregator has whatever retention was cheapest. Records that should be gone survive in four places, and sometimes records that should be preserved get deleted from a backup tier on a ninety-day default, which is a failure in the other direction and much harder to notice.
The wrong identity in the audit trail. Support impersonation implemented by assuming the customer's session, so every entry names the customer. Background jobs writing as a service account with no record of what triggered them. Bulk operations attributing a thousand changes to whoever started the batch, with no link back. In each case the trail is present and populated, which is why it survives review, and in the first case it is actively wrong in the direction that conceals staff activity.
Customer data outside the controlled path. The controls are on the application. The data is not only in the application. It is in the error payloads sent to a third-party monitoring service, in the CSV somebody generates monthly and emails, in the analytics warehouse, in a support tool that syncs records for context, and on a laptop where an engineer once pulled a production sample to debug something. Each of those was a reasonable decision. Together they are the reason "all customer information held or transmitted" [1] is a broader claim than most teams have verified.
MFA on the customer login only. Enabled on the customer login, absent on the internal admin console, the reporting tool, the log viewer, and the database bastion. The rule says any individual accessing any information system [1], and the internal tools frequently expose more than the customer product does.
Access that only accumulates. Nobody has computed effective permissions per person across the application, the database, and cloud IAM. Role definitions are clean. Assignments have been changing without review for years. This is covered in more depth on access control and least privilege.
Why an external look finds them
We are not smarter than the teams whose systems we audit, and it would be obnoxious to imply otherwise. The advantage is structural and comes down to two things.
We arrive without the design in our heads. The team knows how the system is supposed to work, and that knowledge is genuinely useful for almost everything except this, where it causes you to see the intended path rather than the actual one. Someone reading the code with no prior knowledge traces what happens, not what was meant.
And we start from the obligations rather than from the system. The starting direction matters more than it sounds. Starting from the system produces the question "is this controlled," which the team answers from memory. Starting from the obligation produces "show me the mechanism that guarantees this, and let me watch it run," which memory cannot answer.
That second one is available to any team willing to write its obligations down first, which is the point of how to translate a regulation into engineering requirements. Most of the value here is not in hiring someone. It is in the direction you approach from.
What to do about it
The sequence that works is short.
Write the obligations down as system requirements, with the regulatory text quoted next to each. Then, for each one, find the mechanism in the running system that guarantees it, and if there is no single mechanism, write that down too, because "enforced in four places by convention" is a finding. Then turn each into a check that runs on every change, so the gap cannot silently reopen after you close it. Then enumerate every place covered data is stored, because that list is almost always longer than the team expects and it is where the retention and encryption gaps are.
That is the process described in how to audit an existing financial application.
The uncomfortable part is that the finding usually arrives during something else. It arrived during a scoping exercise in the engagement above, and the team's reaction was the one we see: not defensiveness, just the recognition that nobody had ever been asked to show the mechanism. Being asked is most of it.
If you would rather find out at a time you choose than during an examination, get in touch.
Best for
- Applications in production for more than a couple of years, especially after several team changes
- Teams preparing for an examination, a diligence process, or an enterprise security review
- Products where internal tooling has grown alongside the main application
Avoid if
- The product has not launched and holds no real customer data yet, in which case building the checks in is cheaper than auditing for them later
Check before you decide
- Ask what records a read of a customer record produces, and through which interfaces
- List every place a covered record is stored, then compare each one's retention against the policy
- Check the actor recorded when support impersonates a customer
- Check whether MFA covers internal tools, not just the customer login
Common questions
How can a team not know their own application is non-compliant?
Because self-review compares the system against its own design, and the gaps are almost never in the designed part. They sit in the paths added later that bypass the component carrying the control, and in copies of data created for unrelated reasons, so a walkthrough of the intended architecture finds nothing.
Why do these gaps survive for years?
Because they produce no symptom. A missing read log breaks nothing, generates no support ticket, and moves no revenue, and every mechanism a team has for discovering problems depends on something going visibly wrong.
What are the most common gaps in a financial application?
Logging built for writes so reads are invisible, retention enforced in the interface while copies live in replicas and backups with their own lifecycles, the wrong identity recorded in the audit trail during support impersonation, customer data sitting outside the controlled path in monitoring and exports, and MFA on the customer login but not on internal tools.
Why does an external audit find things the team could not?
Not because of greater skill, but because of direction and ignorance of the design. Starting from the obligation and asking to see the mechanism run is a question memory cannot answer, whereas starting from the system produces 'is this controlled', which the team answers from what they intended.
Can a team do this themselves?
Yes, and the main requirement is willingness to write the obligations down before looking at any code, so there is a specification to review against. Most of the advantage here comes from the direction of approach rather than from who is doing it.
Is finding compliance gaps a sign a team was careless?
Generally no. The pattern described on this page is structural: a self-review checks the system against its own design, and gaps sit in the parts nobody designed, such as a path added later that bypasses a control built for an earlier version of the product. A competent team can review its own application carefully and still miss exactly the gaps this page describes, because the review method itself cannot see them.
Why does a missing read log matter if nothing appears broken?
Because the risk a read log exists to catch produces no functional symptom at all. Someone with legitimate access reading far more customer data than their role requires does not trigger an error, a support ticket, or a change in revenue, so a gap in read logging can persist for a long time with nothing in the running application ever signalling that it is there.
What should come first, fixing a gap or writing a check for it?
Write the obligation down as a specification first, then fix the gap, then add a check that fails if the gap returns. Fixing a gap without a check addresses the one instance found during review, while the check is what prevents the same class of gap from quietly reopening the next time a new code path is added.
Related reading
The early warning signs your software project is in trouble
A software project rarely fails in one dramatic moment. It falls behind slowly, and the early signs are easy to explain away. Here are the ones to watch and what to do about each before it is too late.
A practical pre-launch security review for a small team
You do not need perfect security to launch. You need to check the few basics that find most real problems, and to know when the risk is big enough to bring in a specialist.
The real cost of shipping unverified code
The cost of unverified code does not arrive as a bug report. It arrives as a codebase nobody will touch, a review queue that never empties, and a team that has stopped trusting its own pipeline.