Compliance Automation and Tooling Questions
Using technology to make compliance continuous rather than point-in-time. Covers GRC platforms and control libraries (build versus buy, one control set mapped to many regulations, requirement-to-evidence traceability, risk roll-ups), compliance-as-code and policy-as-code checks mapped to framework controls (including testing, versioning and retiring the rules, and turning local legal and privacy obligations such as retention and masking into machine-checkable tests), continuous control monitoring and drift alerts, automated evidence collection and tamper-evident evidence pipelines for auditors, automated compliance and privacy reporting with traceable lineage (including regulatory SLA tracking such as DSAR deadlines), and wiring compliance checks into engineering pipelines without slowing teams. Focuses on what to automate first, how to map controls to machine-checkable tests, where to place checks, and the limits of automation. Does not cover designing or testing the controls themselves, the content of individual frameworks, hands-on security mechanics such as secrets, IAM, scanning or logging pipelines, or conducting the audit.
A global enterprise wants one control library that maps to many regulations and frameworks, and regulations keep changing. How would you decide between buying a GRC platform and building your own, how would you structure the crosswalk so one change updates everything affected, and which systems would you integrate first?
Sample Answer
Direct answer
I would buy a GRC platform for the workflow, evidence collection and auditor-facing views, and build a thin layer of my own for what must stay portable: the unified control library and the crosswalk, stored as structured data I own and can export. Buy the commodity, own the model. That avoids a year of building ticket-and-workflow software while avoiding lock-in on the part that encodes the company's own control decisions.
Key terms
- GRC (governance, risk and compliance): software that holds controls, risks, policies, evidence and audit workflows.
- Crosswalk: a mapping that links one control to every framework requirement it satisfies.
- Unified control library: one set of company controls, each with one owner and one test, mapped to many frameworks, instead of separate control sets per framework.
- SOC 2: an auditor's report on whether a company's security controls work, issued under the American Institute of CPAs (AICPA) criteria. ISO 27001: the international standard for running an information security management system. NIST SP 800-53: a US government catalog of security controls, where IA-2(1) is the entry requiring multi-factor authentication (a second proof of identity beyond a password) for privileged accounts.
- Lock-in: being unable to leave a vendor without losing, or having to rebuild, your own data and decisions.
Buy versus build
| Factor | Leans buy | Leans build |
|---|---|---|
| Frameworks | Several common published ones (the SOC 2, ISO 27001 and NIST frameworks above) | Unusual or internal-only |
| Team | Small compliance team, few engineers to spare | Strong platform team, existing data stack |
| Integrations | Vendor has connectors for your systems | Mostly homegrown systems |
| Time to first audit | Soon | Flexible |
| Cost | License plus administration | Engineers plus maintenance forever |
| Lock-in | Can export controls and mappings | Full control |
Illustrative cost, same annual basis: buy is a 120k license plus half an administrator at 150k loaded (salary plus benefits and overhead), which is 75k, 195k per year; build is 1.5 engineers at 150k, 225k per year, before the cost of maintenance and not shipping product work. The numbers are invented for the comparison; the point is that costs are close, so capability and time decide. I would flip to build if the estate is mostly custom systems the vendor cannot connect to, or no vendor can model the company's frameworks.
Crosswalk structure so one change updates everything
Keep four linked layers, each versioned:
- Framework requirement: framework, version, clause id, text.
- Mapping (many to many: one requirement can map to several controls, and one control to several requirements): requirement to unified control, with strength (full or partial), reviewer and date.
- Unified control: statement, owner, test, systems in scope.
- Test and evidence: result, timestamp, artifact.
Filled in for one control (illustrative values), the four layers look like this:
| Layer | Row |
|---|---|
| Requirement | NIST SP 800-53 r5, IA-2(1), "multi-factor authentication for access to privileged accounts" |
| Requirement | ISO/IEC 27001:2022, Annex A 8.5, secure authentication |
| Requirement | SOC 2, CC6.1, logical access security |
| Mapping | IA-2(1) to CTL-MFA-ADMIN, full, reviewer j.doe, 2026-09-12 |
| Mapping | A.8.5 to CTL-MFA-ADMIN, full, reviewer j.doe, 2026-09-12 |
| Mapping | CC6.1 to CTL-MFA-ADMIN, partial (CC6.1 also needs other controls), reviewer j.doe, 2026-09-12 |
| Control | CTL-MFA-ADMIN, "All administrator accounts require MFA", owner iam-team, test admin_mfa_enforced, systems idp-prod |
| Test and evidence | 2026-10-03, pass, 0 of 41 admins without MFA, artifact a1f09c |
If a framework later renumbers A.8.5, only that requirement row and its mapping row change. The control, its test and the evidence history stay untouched.
Because requirements point at controls rather than at each other, changing one control's test updates every framework it supports. A new framework revision adds new requirement rows, and the diff immediately lists controls with unmapped new requirements. Where available, export in an open format; NIST's OSCAL (Open Security Controls Assessment Language) offers machine-readable catalogs, baselines and assessment data in XML, JSON and YAML.
Using automation to suggest mappings
A language model or text-similarity tool can propose candidate mappings. Store them as suggested, never approved. A person reviews each one; ambiguous requirements are marked partial and escalated to the control owner, counsel or the auditor, who decides what counts. Track the acceptance rate (the share of suggested mappings that reviewers approve), since a low rate means the suggestions are noise.
Which systems to integrate first
- Identity provider: access, MFA and joiner-leaver evidence supports many controls.
- Cloud account and asset inventory: defines scope and supplies configuration checks.
- Ticketing and change management: remediation and approval evidence.
- SIEM (security event log platform): logging and detection evidence, after the basics.
The reason is coverage per integration: identity and inventory feed the most controls for the least effort.
Worked example
Unified control "admins use multi-factor authentication" maps to NIST SP 800-53 IA-2(1) and to the matching SOC 2 (CC6.1) and ISO 27001 (Annex A 8.5) requirements, as the three mapping rows above show. One check against the identity provider updates all three.
Pitfall
Starting with per-framework control lists inside the tool, which recreates the duplication the unified library exists to remove.
Legal requires that data retention, access and masking rules be enforced consistently across your AWS and GCP data platforms, and today they live in wiki pages and tribal knowledge. How would you turn those requirements into machine-checkable policy, decide where each rule is evaluated, assign ownership of the policies, and show leadership that adherence is actually improving?
Sample Answer
Direct answer
I would translate each wiki sentence into a rule with four parts (the next section shows one sentence converted): the data it applies to (via classification tags, labels such as classification=restricted attached to a dataset), the attribute to measure, the pass condition, and the point where it is checked. I would then split enforcement by what each platform can actually prevent, assign each policy an owner who is accountable for it, and report adherence with the denominator visible, so improvement means more compliant data, not less data in scope.
From wiki page to machine-checkable policy
| Requirement | Machine-checkable form | AWS example | GCP example | Evaluated |
|---|---|---|---|---|
| Retention | Every dataset tagged with a retention period has a lifecycle or expiry setting no longer than that period | S3 lifecycle expiration rule | Cloud Storage lifecycle delete rule, BigQuery table or dataset expiration | Plan time for provisioned resources, plus scheduled scan |
| Access | No public or all-users principal on restricted data. Access granted to groups, not individuals (IAM, identity and access management, is each cloud's permission system) | Bucket and IAM policies with a wildcard principal | IAM bindings with allUsers or allAuthenticatedUsers | Plan time and scheduled scan |
| Masking (showing a scrambled or partial value, such as ****1234, to people who may query a column but are not cleared for the real value) | Restricted columns carry a masking or column-level access policy (only named groups can read the column at all) | Lake Formation column-level filters | BigQuery policy tags with data masking | Presence by scan. Behaviour by test query |
The cells show one example per cloud of how each requirement can be met; the same rule shape maps onto whichever feature a platform offers, so nothing here is a list to memorize.
One wiki sentence, converted
Take the wiki line "Customer personal data is deleted after 7 years." It becomes a rule with the four parts filled in:
Applies to: datasets tagged data_class=personal
Measure: the expiry or lifecycle-delete setting on the dataset, in days
Pass if: the setting exists and is at most 2555 days (7 years x 365 days; the
legal team states in the policy whether leap days are counted)
Checked: at plan time for resources Terraform creates, and by a weekly scan
of live inventory for everything else
Every sentence on the wiki gets the same four slots. A sentence that cannot fill them, such as "handle data carefully", is a sign the requirement needs rewording with the legal owner before it can be automated. A bucket tagged personal with no expiry setting, or an expiry of 4,000 days, fails; an untagged bucket is reported as a classification gap rather than silently passing.
A rule whose input is a classification tag needs classification to exist first. Without it the policy has nothing to evaluate, so an untagged dataset is itself a finding.
Where each rule is evaluated
- Plan time (before deploy, when the infrastructure code is previewed and nothing exists yet): best for settings Terraform manages, because it stops a mistake before it exists.
- Runtime scan: needed for anything created outside Terraform and for drift. Drift means a live setting differing from what the code says, for example a hand edit in a console. Use the clouds' own inventory services (AWS Config and Google Cloud Asset Inventory, each of which records the resources in an account or project and their settings) as the data source.
- Data plane: masking and access are enforced by the platform at query time. Scanning confirms the policy is attached, but only a test query run as a low-privilege principal confirms the output is actually masked.
- Effectiveness check for retention: a lifecycle rule existing is not proof data was deleted. Periodically count objects or rows older than the retention period, and note that backups, replicas and derived tables are separate copies to cover.
Ownership
- Legal and privacy own the requirement text and the retention schedule.
- Data governance owns classification.
- Platform or security engineering owns the policy code and checks, in a repository where requirement-changing pull requests need legal approval.
- The data owner for each dataset is accountable for remediation and requests any exception.
Showing leadership that it is improving (illustrative numbers)
Suppose 400 datasets are in scope. In the first month 320 are classified (80%) and 256 of those are compliant. Later, 360 are classified (90%) and 306 are compliant.
| Classified | Compliant | Compliant, of classified | Compliant, of all 400 | |
|---|---|---|---|---|
| Month 1 | 320 (80%) | 256 | 80.0% | 64.0% |
| Later | 360 (90%) | 306 | 85.0% | 76.5% |
Showing only the second-last column would flatter a team that classified fewer datasets. Report coverage and compliance together, with the trend, exception count and age, and time to remediate.
Caveat
A check proves a setting, not that the rule is legally sufficient. Whether a retention period or a masking approach satisfies a regulation is a decision for legal counsel, and the policy encodes their decision.
Your company passes its annual audit every year, yet a misconfiguration sat live for months between audits and nobody noticed. How would you explain to a newer engineer why point-in-time compliance fails, and which checks would you automate first to make compliance continuous?
Sample Answer
Direct answer
An annual audit is a snapshot: it checks a sample of evidence from a window and passes you if what it looked at was in order. It says little about the days it did not look at. Continuous compliance replaces the occasional look with automated checks that run all the time, so a problem is found in hours or days, not at the next audit.
An analogy for a newer engineer
A yearly car inspection can pass a car whose brake warning light came on in March. Continuous compliance is the warning light: it tells you when the problem starts. The inspection still matters, because it checks that the light itself works.
Why point-in-time fails
- Sampling: auditors test a sample, meaning a small subset of records picked from the period, not every day of it.
- Preparation effect: teams tidy up before the audit, so the audited state is not the typical state.
- Gap: if a misconfiguration is live for 90 days, that is 90 of 365, about 24.7% of the year, with nobody watching.
- Cost of lateness: a problem found months later has had months to be exploited and is harder to trace.
What I would automate first
A baseline is the approved list of settings a system should have, so most of these checks are comparisons against it. Pick checks that are objective, frequent and costly when wrong:
- Configuration baselining: encryption on, public exposure off, admin MFA on, logging on. The check compares live state to that baseline daily.
- Change management evidence: every production change links to a reviewed, approved pull request, and the pipeline flags changes with no approval.
- Immutable artifacts: build once, sign, and deploy that exact artifact; servers are replaced, not hand-edited. An artifact is the packaged build output (a container image or a program file). Signing means the build system attaches a cryptographic stamp made from the artifact's contents and a secret key, and the deploy step refuses anything whose stamp does not verify. If someone edits the file by hand, its contents change, the stamp no longer matches, and the edit becomes a detectable difference. The artifact's hash (a short fingerprint of its bytes) recorded at deploy time shows exactly what was deployed.
- Alert health: check that logging and alerting still work, since silent failure here undermines all the others.
For example, a change record might read: change 4812, reviewed and approved by a named second person, merged at 14:02, artifact hash recorded, deployed at 14:09 (illustrative). For alert health, a daily test event must appear in the alerting system within a set time, and the record shows the last pass. These signals double as audit evidence: baseline scan results, change records with approvals and artifact hashes are exactly what an auditor asks for, already timestamped.
Worked example
The annual audit passed in January. A storage bucket became public in March and stayed so until July. A daily baseline check would have failed on day one in March, opened a ticket, and the fix would be visible as evidence. The July discovery would not exist.
Caveats
Automation covers what can be measured; policies, training and judgement calls still need people. Start with a handful of checks that matter, not a hundred that nobody reads.
An auditor asks you to trace one requirement to the control that meets it, to the implementation, and to the evidence, and to show what changed since last audit. How would you model that traceability in your GRC tooling so these reports are generated rather than assembled by hand, and what breaks when a framework or a system is revised?
Sample Answer
Direct answer
Model the chain as linked, versioned records (requirement to control to implementation to test to evidence) with effective dates on every record, so any report is a query, not a document assembled by hand. "What changed since last audit" is a query on version and effective-date fields between two dates.
The model
| Entity | Key fields | Notes |
|---|---|---|
| Requirement | framework, version, clause id, text | Never edited; a revision is a new row |
| Control | control id, version, statement, owner, effective-from (the date this version takes effect) | Each change is a new version |
| Mapping | requirement, control, strength (full or partial), reviewer | Many to many |
| Implementation | control, system, check name | Where the control lives |
| Evidence | control, control version tested, system, collected-at, result, artifact hash | Append-only (rows are only ever added, never edited or deleted) |
Worked example (code run, output pasted)
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE requirement (req_id TEXT, framework TEXT, version TEXT, clause TEXT, text TEXT, PRIMARY KEY (req_id, version));
CREATE TABLE control (control_id TEXT, version INT, statement TEXT, owner TEXT, effective_from TEXT, PRIMARY KEY (control_id, version));
CREATE TABLE mapping (req_id TEXT, req_version TEXT, control_id TEXT, strength TEXT);
CREATE TABLE implementation (control_id TEXT, system TEXT, check_name TEXT);
CREATE TABLE evidence (control_id TEXT, control_version INT, system TEXT, collected_at TEXT, result TEXT, artifact_sha TEXT);
""")
db.executemany("INSERT INTO requirement VALUES (?,?,?,?,?)", [
("NIST-IA-2.1", "NIST SP 800-53", "r5", "IA-2(1)", "MFA for privileged accounts")])
db.executemany("INSERT INTO control VALUES (?,?,?,?,?)", [
("CTL-MFA-ADMIN", 1, "Admins use MFA", "iam-team", "2024-01-01"),
("CTL-MFA-ADMIN", 2, "Admins use phishing-resistant MFA", "iam-team", "2025-03-01")])
db.execute("INSERT INTO mapping VALUES ('NIST-IA-2.1','r5','CTL-MFA-ADMIN','full')")
db.execute("INSERT INTO implementation VALUES ('CTL-MFA-ADMIN','idp-prod','admin_mfa_enforced')")
db.executemany("INSERT INTO evidence VALUES (?,?,?,?,?,?)", [
("CTL-MFA-ADMIN",2,"idp-prod","2025-06-30","pass","a1f09c"),
("CTL-MFA-ADMIN",2,"idp-prod","2025-07-01","pass","b72e44")])
print("TRACE: requirement -> control -> implementation -> latest evidence")
for r in db.execute("""
SELECT m.req_id, m.control_id, i.system, i.check_name, e.collected_at, e.result, e.artifact_sha
FROM mapping m JOIN implementation i USING (control_id)
JOIN evidence e ON e.control_id=m.control_id AND e.system=i.system
WHERE e.collected_at = (SELECT MAX(collected_at) FROM evidence WHERE control_id=m.control_id)"""):
print(r)
LAST_AUDIT = "2025-01-01"
print("CHANGED since last audit (", LAST_AUDIT, ")")
for r in db.execute("SELECT control_id, version, statement, effective_from FROM control WHERE effective_from > ?", (LAST_AUDIT,)):
print(r)
Output:
TRACE: requirement -> control -> implementation -> latest evidence
('NIST-IA-2.1', 'CTL-MFA-ADMIN', 'idp-prod', 'admin_mfa_enforced', '2025-07-01', 'pass', 'b72e44')
CHANGED since last audit ( 2025-01-01 )
('CTL-MFA-ADMIN', 2, 'Admins use phishing-resistant MFA', '2025-03-01')
One query returns the whole chain for NIST SP 800-53 IA-2(1), and a second returns the control that tightened in March. A real "changed" report would also diff mappings, implementations and test definitions, not only the control statement.
Reading the code in plain language. The trace query starts from the mapping row and attaches the implementation with JOIN ... USING (control_id), which means "pair rows that share the same control_id value". It then attaches evidence rows for the same control and system (each evidence row also records which control version it tested, here version 2, so a report can separate evidence gathered against the old wording from proof of the new wording; this short trace query does not filter on that column, and a production version would add a condition that the evidence's control_version equals the control's latest version). The final WHERE keeps only the newest evidence row: for each mapping row, the inner SELECT MAX(collected_at) looks up the latest collection date for that control, and only evidence collected on that date survives (a subquery that depends on the outer row like this is called a correlated subquery). That is why the 2025-06-30 row is dropped and the 2025-07-01 row printed. In the schema, PRIMARY KEY (control_id, version) allows many versions of one control but never the same version twice, so rewriting a control means inserting version 3, not editing version 2. The change query simply lists control versions whose effective date is later than the last audit date; ISO-formatted dates sort correctly as text.
Generating the reports
- Traceability report: for each requirement in scope, list its controls, systems, latest result and evidence link. Coverage is computed: a requirement counts as covered if it has an approved mapping to an active control whose latest test is within its freshness window (the maximum age, for example 7 days, at which a result still counts as current).
- Change report: all rows with a version or effective date after the last audit date, grouped by entity, with reviewer and reason.
- Gaps first: list requirements with no mapping, controls with no test, and tests with stale or missing evidence. These matter more to an auditor than the covered rows.
What breaks when something is revised
| Change | Failure | Defence |
|---|---|---|
| Framework revision (renumbering, new or withdrawn clauses) | Mappings point at old clause ids, new requirements are unmapped, withdrawn ones leave orphan controls | Version requirements, carry mappings forward as proposed, review the diff, report unmapped and orphaned items |
| System renamed, migrated or retired | Test points at a resource that no longer exists, so no evidence arrives | Treat missing evidence as a failure, not a pass; alert on no-data |
| Control rewritten | Old evidence no longer proves the new wording | Evidence references control version; re-test after change |
| Many mappings at scale | Recomputing coverage over the whole catalog is slow | Recompute only the subgraph touched by a change (the connected slice of requirements, controls and tests reachable from the changed row), keep a materialized coverage table (a stored, precomputed result refreshed when inputs change, instead of recalculated on every report) |
A concrete renumbering. In the ISO/IEC 27001 move from the 2013 to the 2022 edition, the control "A.9.4.2 Secure log-on procedures" became "A.8.5 Secure authentication". With versioned requirements, the 2022 row is added, the old mapping to CTL-MFA-ADMIN is carried forward as proposed, and a reviewer approves it. A withdrawn clause that nothing replaces leaves a control with no requirement pointing at it, which is an orphan control in the table above, and it appears in the gaps list for a human decision.
Pitfalls
- Editing rows in place destroys the "what changed" answer; append and version instead.
- A green coverage number built on stale evidence is the same silent-failure problem as an unchecked dashboard, so show freshness next to coverage.
Your company runs separate risk registers per product line and region, so leadership cannot see aggregate risk or compliance posture. How would you design the data model and ingestion for a GRC platform that rolls these up automatically, how would you keep scoring consistent across teams, and which executive views would you build from it?
Sample Answer
Direct answer
Build one canonical model (business unit, risk, control, link between them) with a single taxonomy (one shared list of risk categories) and a single anchored scoring rubric (a scoring guide that ties each level from 1 to 5 to concrete descriptions, so two people land on the same number), and ingest each local register through a mapping layer that normalizes into it. Roll up with worst-case and counts, never averages: nine risks scored 2 and one scored 15 average 3.3, which looks harmless, while the worst case (15) and the count of high risks (1) show the problem. Put three executive views on top. The hard part is not the tables. It is getting teams to score the same way and keeping the data fresh.
Data model (core record fields)
- business_unit: product line and region.
- risk: taxonomy category, owner, status (open, treating, accepted, closed), inherent likelihood and impact, residual likelihood and impact (inherent is before controls, residual after), last reviewed date, source system and source id.
- control and a risk_control link: last test result and date.
- history: a snapshot taken daily so trends exist.
Ingestion
- Extract from each register (spreadsheet, ticket tool, an existing GRC tool) on a schedule or via API.
- Map local categories to the shared taxonomy through a mapping table owned by risk management. An unmapped category is quarantined (held in a review queue) with an alert, not dropped.
- Validate: scores 1 to 5, owner present, review date present. Rejects go back to the register owner.
- Upsert (insert a new row, or update it if it already exists) keyed by source system plus source id, so reruns do not duplicate rows.
- Link duplicates: one vendor risk appearing in three products should point to a parent risk so the roll-up does not triple count it.
Consistent scoring
Use one 5x5 grid where score = likelihood x impact and high means 15 or above. Anchor each level with descriptors (for impact, ranges of financial loss, records exposed or downtime that leadership approved), run calibration sessions (meetings where teams score the same sample risks, compare, and discuss each difference until they converge), and have risk management re-score a small random sample each quarter. Show both the team's score and the re-scored one, so disagreement is visible rather than argued over. Roll-up uses ordinal scores (numbers that only rank risks, where the gaps between levels are not equal amounts), so do not sum them (ten minor risks scoring 2 each sum to 20, which would wrongly outrank one severe risk scoring 15), and do not mix them with quantified loss estimates from units that use FAIR (a method that estimates loss in money).
Worked roll-up
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE business_unit (id TEXT PRIMARY KEY, product_line TEXT, region TEXT);
CREATE TABLE risk (
id TEXT PRIMARY KEY, unit_id TEXT REFERENCES business_unit(id),
category TEXT, -- from the shared taxonomy
owner TEXT, status TEXT, -- open / treating / accepted / closed
likelihood INTEGER CHECK (likelihood BETWEEN 1 AND 5),
impact INTEGER CHECK (impact BETWEEN 1 AND 5),
residual_likelihood INTEGER CHECK (residual_likelihood BETWEEN 1 AND 5),
residual_impact INTEGER CHECK (residual_impact BETWEEN 1 AND 5),
last_reviewed TEXT
);
CREATE TABLE control (id TEXT PRIMARY KEY, last_test_result TEXT); -- pass / fail
CREATE TABLE risk_control (risk_id TEXT, control_id TEXT);
""")
db.executemany("INSERT INTO business_unit VALUES (?,?,?)", [
("BU1", "payments", "EU"), ("BU2", "payments", "US"), ("BU3", "lending", "EU")])
db.executemany("INSERT INTO risk VALUES (?,?,?,?,?,?,?,?,?,?)", [
("R1", "BU1", "third_party", "ana", "open", 4, 5, 3, 5, "2026-09-20"),
("R2", "BU2", "third_party", "bo", "treating", 3, 4, 2, 4, "2026-06-01"),
("R3", "BU3", "data_privacy", "cy", "open", 5, 3, 3, 3, "2026-09-28"),
("R4", "BU1", "access", "ana", "accepted", 2, 5, 2, 5, "2026-09-01"),
])
db.executemany("INSERT INTO control VALUES (?,?)", [("C1","pass"),("C2","fail")])
db.executemany("INSERT INTO risk_control VALUES (?,?)", [("R1","C1"),("R2","C2"),("R3","C1")])
print("Residual score = residual_likelihood x residual_impact; HIGH means score >= 15")
q = """
SELECT b.product_line, b.region, r.category,
COUNT(*) AS risks,
MAX(r.residual_likelihood * r.residual_impact) AS worst_residual,
SUM(r.residual_likelihood * r.residual_impact >= 15) AS high_count,
SUM(julianday('2026-10-04') - julianday(r.last_reviewed) > 90) AS stale_reviews
FROM risk r JOIN business_unit b ON b.id = r.unit_id
WHERE r.status <> 'closed'
GROUP BY b.product_line, b.region, r.category
ORDER BY worst_residual DESC, b.product_line, b.region
"""
for row in db.execute(q):
print(row)
print("Open risks with no linked control, or only failing controls:")
q2 = """
SELECT r.id, r.status FROM risk r
WHERE r.status <> 'closed' AND NOT EXISTS (
SELECT 1 FROM risk_control rc JOIN control c ON c.id = rc.control_id
WHERE rc.risk_id = r.id AND c.last_test_result = 'pass')
ORDER BY r.id
"""
for row in db.execute(q2):
print(row)
Residual score = residual_likelihood x residual_impact; HIGH means score >= 15
('payments', 'EU', 'third_party', 1, 15, 1, 0)
('payments', 'EU', 'access', 1, 10, 0, 0)
('lending', 'EU', 'data_privacy', 1, 9, 0, 0)
('payments', 'US', 'third_party', 1, 8, 0, 1)
Open risks with no linked control, or only failing controls:
('R2', 'treating')
('R4', 'accepted')
How the SQL works: the comparison inside SUM(...) is true (counted as 1) or false (counted as 0) for each row, so summing it counts how many rows meet the condition, here scores of 15 or more or reviews older than 90 days. julianday turns a date into a day number so that subtracting two dates gives the number of days between them. GROUP BY produces one output row per product line, region and category, with MAX giving the worst score in each group. The second query lists open risks for which no linked control has a passing test.
Reading the output: payments in the EU has one high residual risk (R1 scores 3 x 5 = 15, inherent 4 x 5 = 20). R2 is the only stale one: reviewed on 2026-06-01, 125 days before 2026-10-04, against a 90-day rule. R2 and R4 have no passing control: R2's only control failed its test, and R4 is accepted with no control, so its acceptance record is what the platform should surface.
Executive views
- Residual heat map (a grid colored by likelihood and impact) by product line and region, showing count of high risks and the worst score (not the mean).
- Top risks above appetite (risk appetite is the level of risk leadership agrees to accept; here, a residual score of 15 or more is above it), with owner, age, treatment status and next date.
- Control health and data quality: failed controls, risks with no passing control, share of risks with an owner, share reviewed in the last 90 days.
Limits
A rolled-up number is only as honest as its inputs. A register with stale reviews will look calm, which is why staleness is a first-class metric. Automatic aggregation cannot tell whether a team under-scored, which is what calibration is for.
Unlock Full Question Bank
Get access to all 10 Compliance Automation and Tooling interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.