Skip to content
academia.sh

Lesson 12 / 17

The Function of Auditor and Compliance Roles

Measuring independent assurance in terms of separation of duties: of the 17 real findings in 24 controls, only 3 are visible under self-review, and 12 of the 14 invisible findings stay hidden because of independence; the compliance function sees 8, independent internal audit 17, and sampled external audit 9 findings; full separation surfaces 14 findings while bringing 72 audit-days, 19.2 extra work-days, and 48 extra evidence items; and the rankings by findings seen and by days per finding swap places at the two ends.

Published Updated

In this lesson

  • Distinguishes assurance arrangements along the depth and independence dimensions and counts the findings each arrangement sees
  • Measures the cost of separation of duties in three items: audit-days, extra approval and handoff work-days, and extra evidence items
  • Compares partial separation with full separation and shows where the findings-per-day rate is highest
Contents

Every number in the previous lesson rested on an assumption: the cells said to be filled really are filled, the procedure said to produce records really does produce them. The source of this assumption is clear — the party filling in the table, writing the documents, and reporting that the records were produced is the same party. Everything measured throughout the topic ran on the institution’s own self-declaration.

This lesson removes that assumption and measures independent assurance. Independent assurance is not an adjective; it is a countable arrangement with two dimensions. The first is depth: how much technical review is needed for a finding to be seen — is looking at the record enough, does it need to be cross-checked against a second source, or does the work need to be independently rerun. The second is independence: the distance between the person who will report the finding and the owner of the finding. A finding becomes visible only when both conditions are met at once.

  • FW39. The control set is 24 items spread across six teams; 17 of them have a genuine nonconformity. The set and the findings are fictional.
  • FW40. Every finding has a depth requirement (1 visible from the record, 2 requires a cross-check, 3 requires an independent rerun) and an independence requirement (0 whoever looks sees it, 1 requires a separate team, 2 requires a separate reporting line).
  • FW41. Four assurance arrangements are compared: self-review (depth 1, independence 0), the compliance function (2, 1), independent internal audit (3, 2), external audit (3, 2 but sampled).
  • FW42. External audit looks at 40 percent of the controls and picks its sample from the controls with the most evidence items.
  • FW43. Audit-days per control range from 0.5 to 4.0 depending on the arrangement (fictional).
  • FW44. When separation is applied, every control brings one extra approval step (0.4 days) and one handoff cost per independence level (0.2 days); every independent review produces two extra evidence items in the period.
  • FW45. Partial separation means applying independent audit to the n controls with the highest evidence volume, and self-review to the rest.
// role/assurance.mjs — the findings each independent assurance arrangement sees.
// MODEL: regional measurement network (fictional); controls, findings, and days are fictional.
export const row = (s) => s.trim().split("\n").map((l) => l.trim().split("|"));
export const gen = (B) => (...h) => h.map((v, j) =>
  (j ? String(v).padStart(B[j]) : String(v).padEnd(B[0]))).join("");

// code | owning team | has finding? | depth (1-3) | independence (0-2) | evidence items
export const CONTROL = row(`
  C01|infrastructure|yes|1|0|240
  C02|infrastructure|yes|2|1|180
  C03|infrastructure|no|1|0|60
  C04|infrastructure|yes|3|2|48
  C05|infrastructure|yes|1|1|365
  C06|application|yes|2|0|52
  C07|application|no|1|0|12
  C08|application|yes|3|1|310
  C09|application|yes|1|2|30
  C10|application|no|2|1|24
  C11|identity|yes|2|2|120
  C12|identity|yes|1|0|48
  C13|identity|no|3|1|4
  C14|identity|yes|3|2|365
  C15|data|yes|1|1|365
  C16|data|no|2|0|30
  C17|data|yes|2|2|12
  C18|data|yes|3|0|4
  C19|field|yes|1|2|18
  C20|field|no|1|1|9
  C21|field|yes|2|1|3
  C22|vendor|yes|3|2|3
  C23|vendor|no|2|2|2
  C24|vendor|yes|1|0|1`)
  .map(([code, team, finding, d, b, item]) => ({ code, team, finding: finding === "yes",
    d: +d, b: +b, item: +item }));

// arrangement | depth it can see | independence it carries | days per control | sample ratio
export const ARRANGEMENT = row(`
  self-review|1|0|0.5|1
  compliance function|2|1|1.5|1
  independent internal audit|3|2|3.5|1
  external audit|3|2|4.0|0.4`)
  .map(([name, d, b, days, sample]) => ({ name, d: +d, b: +b, days: +days, sample: +sample }));

export const FINDING = CONTROL.filter((k) => k.finding);
// External audit's sample: the controls with the most evidence items are selected (model assumption).
export const scope = (z) => z.sample === 1 ? CONTROL
  : [...CONTROL].sort((a, b) => b.item - a.item).slice(0, Math.round(CONTROL.length * z.sample));
export const seen = (z) => scope(z).filter((k) => k.finding && k.d <= z.d && k.b <= z.b);

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`model: ${CONTROL.length} controls, ${FINDING.length} real findings, ` +
    `${ARRANGEMENT.length} assurance arrangements`);

  const y = gen([28, 10, 10, 12, 12, 9, 16]);
  console.log("\n" + y("assurance arrangement", "scope", "seen", "unseen", "seen %", "days",
    "days / finding"));
  for (const z of ARRANGEMENT) {
    const g = seen(z), days = scope(z).length * z.days;
    console.log(y(z.name, scope(z).length, g.length, FINDING.length - g.length,
      (100 * g.length / FINDING.length).toFixed(1), days.toFixed(1),
      g.length ? (days / g.length).toFixed(2) : "-"));
  }

  const z2 = gen([14, 10, 13, 12, 16]);
  console.log("\n" + z2("independence", "findings", "self-review", "compliance", "internal audit"));
  for (const b of [0, 1, 2]) {
    const group = FINDING.filter((k) => k.b === b);
    console.log(z2(`level ${b}`, group.length,
      ...ARRANGEMENT.slice(0, 3).map((z) => group.filter((k) => k.d <= z.d && k.b <= z.b).length)));
  }
  const hidden = FINDING.filter((k) => !seen(ARRANGEMENT[0]).includes(k));
  console.log(`\nof the ${hidden.length} findings not visible under self-review, ` +
    `${hidden.filter((k) => k.b > 0).length} stay hidden because of independence, ` +
    `${hidden.filter((k) => k.b === 0).length} because of depth`);
  const sG = [...ARRANGEMENT].sort((a, b) => seen(b).length - seen(a).length).map((z) => z.name);
  const sV = [...ARRANGEMENT].sort((a, b) => (scope(a).length * a.days) / (seen(a).length || 1) -
    (scope(b).length * b.days) / (seen(b).length || 1)).map((z) => z.name);
  console.log(`by findings seen : ${sG.join(" > ")}`);
  console.log(`by day per finding: ${sV.join(" > ")}`);
}
model: 24 controls, 17 real findings, 4 assurance arrangements

assurance arrangement            scope      seen      unseen      seen %     days  days / finding
self-review                         24         3          14        17.6     12.0            4.00
compliance function                 24         8           9        47.1     36.0            4.50
independent internal audit          24        17           0       100.0     84.0            4.94
external audit                      10         9           8        52.9     40.0            4.44

independence    findings  self-review  compliance  internal audit
level 0                5            3           4               5
level 1                5            0           4               5
level 2                7            0           0               7

of the 14 findings not visible under self-review, 12 stay hidden because of independence, 2 because of depth
by findings seen : independent internal audit > external audit > compliance function > self-review
by day per finding: self-review > external audit > compliance function > independent internal audit

The first row of the first table is the lesson’s headline number. When the team that built a control audits its own control, only three of the 17 real findings are visible; 14 are never reported. This requires no assumption of bad faith: the same team looks with the same assumptions, reads the same record the same way, and never asks a question outside its own design. The second table breaks this down by cause — of the 14 invisible findings, 12 stay hidden because of independence, two because of depth. The problem is overwhelmingly one of position, not competence.

The rows of the second table spell this out further. Of the five findings that require no independence at all, three are visible under self-review; none of the level-1 or level-2 findings are visible. The compliance function — a separate team, but on the same management line — opens up level 0 and level 1, yet still cannot see any of the seven level-2 findings. Seven findings surface only with a separate reporting line. What determines the independence a finding needs is not the size of the finding, but who it would make look bad.

The last two lines carry the topic’s measurement axis into this lesson, and the two rankings swap places at their two ends; the relative order of the two middle arrangements stays the same in both. By findings seen, independent internal audit ranks first; by day per finding, it ranks last. Self-review spends 4.00 days per finding it sees, independent internal audit 4.94. Read on its own, this number makes self-review look efficient; but self-review’s cheapness is a byproduct of the fact that the three findings it sees are the easiest ones to see. An arrangement that finds the easy findings and never looks for the hard ones will always win on cost per finding. Choosing an assurance arrangement by unit cost alone rewards blindness.

The Cost of Separation and Partial Separation

// role/separation.mjs — the findings separation of duties surfaces and the extra work it brings.
import { CONTROL, ARRANGEMENT, FINDING, gen, seen, scope } from "./assurance.mjs";

const APPROVAL = 0.4;  // extra approval step per control after separation (days, fictional)
const HANDOFF = 0.2;   // handoff between teams (days/control, fictional)
const EVIDENCE = 2;    // every independent review produces 2 extra evidence items per period (fictional)

const days = (z) => scope(z).length * z.days;
const extraWork = (z) => (z.b === 0 ? 0 : scope(z).length * (APPROVAL + HANDOFF * z.b));
const extraEvidence = (z) => (z.b === 0 ? 0 : scope(z).length * EVIDENCE);

const y = gen([28, 6, 10, 12, 17, 22]);
console.log(`${CONTROL.length} controls, ${FINDING.length} real findings; under self-review ` +
  `${seen(ARRANGEMENT[0]).length} are visible`);
console.log("\n" + y("assurance arrangement", "seen", "surfaced", "audit days", "extra work days",
  "extra evidence items"));
for (const z of ARRANGEMENT) {
  console.log(y(z.name, seen(z).length, seen(z).length - seen(ARRANGEMENT[0]).length,
    days(z).toFixed(1), extraWork(z).toFixed(1), extraEvidence(z)));
}

// Hybrid arrangement: independent audit on the N controls with the highest evidence volume, self-review on the rest.
const sorted = [...CONTROL].sort((a, b) => b.item - a.item);
const z2 = gen([18, 13, 6, 8, 12, 16]);
console.log("\n" + z2("hybrid threshold", "independent", "seen", "missed", "audit days",
  "days / finding"));
for (const n of [0, 6, 12, 18, 24]) {
  const independent = new Set(sorted.slice(0, n).map((k) => k.code));
  const g = CONTROL.filter((k) => k.finding && (independent.has(k.code)
    ? k.d <= ARRANGEMENT[2].d && k.b <= ARRANGEMENT[2].b
    : k.d <= ARRANGEMENT[0].d && k.b <= ARRANGEMENT[0].b));
  const total = n * ARRANGEMENT[2].days + (CONTROL.length - n) * ARRANGEMENT[0].days;
  console.log(z2(`${n} controls`, n, g.length, FINDING.length - g.length, total.toFixed(1),
    (total / g.length).toFixed(2)));
}

// Findings that stay hidden, per team
const teams = [...new Set(CONTROL.map((k) => k.team))];
const visible = new Set(seen(ARRANGEMENT[0]).map((k) => k.code));
const z3 = gen([16, 10, 10, 9, 8]);
console.log("\n" + z3("team", "controls", "findings", "visible", "hidden"));
for (const e of teams) {
  const k = CONTROL.filter((x) => x.team === e), b = k.filter((x) => x.finding);
  console.log(z3(e, k.length, b.length, b.filter((x) => visible.has(x.code)).length,
    b.filter((x) => !visible.has(x.code)).length));
}
const mostHidden = teams.map((e) => ({ e, n: CONTROL.filter((x) => x.team === e && x.finding &&
  !visible.has(x.code)).length })).sort((a, b) => b.n - a.n)[0];
console.log(`\nteam with the most findings hidden under self-review: ${mostHidden.e} (${mostHidden.n} findings)`);
console.log(`full separation surfaces ${seen(ARRANGEMENT[2]).length - seen(ARRANGEMENT[0]).length} ` +
  `findings; its cost is ${(days(ARRANGEMENT[2]) - days(ARRANGEMENT[0])).toFixed(1)} audit-days, ` +
  `${extraWork(ARRANGEMENT[2]).toFixed(1)} extra work-days, and ${extraEvidence(ARRANGEMENT[2])} ` +
  `extra evidence items`);
24 controls, 17 real findings; under self-review 3 are visible

assurance arrangement         seen  surfaced  audit days  extra work days  extra evidence items
self-review                      3         0        12.0              0.0                     0
compliance function              8         5        36.0             14.4                    48
independent internal audit      17        14        84.0             19.2                    48
external audit                   9         6        40.0              8.0                    20

hybrid threshold    independent  seen  missed  audit days  days / finding
0 controls                    0     3      14        12.0            4.00
6 controls                    6     8       9        30.0            3.75
12 controls                  12    12       5        48.0            4.00
18 controls                  18    14       3        66.0            4.71
24 controls                  24    17       0        84.0            4.94

team              controls  findings  visible  hidden
infrastructure           5         4        1       3
application              5         3        0       3
identity                 4         3        1       2
data                     4         3        0       3
field                    3         2        0       2
vendor                   3         2        1       1

team with the most findings hidden under self-review: infrastructure (3 findings)
full separation surfaces 14 findings; its cost is 72.0 audit-days, 19.2 extra work-days, and 48 extra evidence items

The first table splits the cost of separation into three distinct items, and the three come from different places. Full separation surfaces 14 findings; in exchange it brings 72 audit-days, 19.2 extra work-days, and 48 extra evidence items. Audit-days are the auditor’s work; extra work-days belong to the party being audited — the extra approval steps and the handoffs between teams fall on the team running the control, not on the auditor. The extra evidence items feed directly into the load counted in the third lesson: an independent review produces its own record, and that record also has to be stored, compiled, and sampled. Independence is not free, and two-thirds of its bill goes to the audited party.

The external audit row offers an interesting comparison alongside this. It carries the same depth and the same independence as independent internal audit, but its sample is 40 percent of the controls, and it sees nine of the 17 findings. Because the sample is selected by evidence volume, it carries a systematic bias: controls that produce a lot of records get seen, controls that produce few do not. Since there is no link between evidence volume and the importance of a finding, this is the quietest way of narrowing scope.

The second table maps out partial separation, and the curve is not flat. Applying independent audit to six controls raises findings from three to eight and lowers the cost per finding from 4.00 to 3.75 — better than both full self-review and full separation. Once the threshold reaches 12, the unit cost returns to its starting value, and it climbs further at 18 and 24. The reason is plain: the first controls added are the ones with the highest evidence volume, and finding density is higher among them; each subsequent addition brings in fewer and fewer findings. Independence shows diminishing returns, but the difference between the five, three, and zero findings left uncovered is also real; which one to accept is a decision, and that decision’s number should be written down.

The last table shows where separation does the most good. Under self-review, the infrastructure, application, and data teams each have three findings hidden; the vendor team has only one hidden finding. Separation does not need to be applied to the whole institution at once — starting with the teams that have the highest finding density and the highest secrecy surfaces more findings for the same number of days. Knowing this also requires a measurement, and that measurement is only available once independent audit has been run at least once.

Summary

  • Independent assurance is a two-dimensional arrangement: depth gives how much review a finding demands, independence gives the distance between the reporter and the owner. A finding becomes visible only when both conditions are met at once.
  • When the team that built a control audits its own control, only three of the 17 real findings are visible; of the 14 invisible findings, 12 stay hidden because of independence, two because of depth.
  • The compliance function opens up the level-0 and level-1 findings but cannot see any of the seven level-2 findings: what determines the independence a finding needs is not its size, but who it would make look bad.
  • The ranking by findings seen and the ranking by day per finding swap places at their two ends: self-review spends 4.00 days per finding, independent internal audit 4.94. Self-review’s cheapness is a byproduct of seeing only the easy findings.
  • Full separation surfaces 14 findings and brings a three-part cost: 72 audit-days, 19.2 extra work-days, and 48 extra evidence items. The extra work-days fall on the team running the control, not on the auditor.
  • Partial separation shows diminishing returns: independent audit on six controls raises findings to 8 and lowers the unit cost to 3.75; on 24 controls findings rise to 17, but the unit cost climbs to 4.94.

Next Step

The topic closes here. It opened with a problem: the previous topic’s numbering was written in the institution’s own language and could not be read from the outside. Seven lessons built a common language and counted that language’s cost — a maturity measure split across five functions, a documentation cycle with a written scope, a matrix binding controls to requirements, extra controls tied to data type, rights counted item by item, document levels that do and do not produce evidence, and a separation of duties that makes findings visible. Each one had its number written down, and each one showed the same thing: mapping is a claim, evidence is a separate item.

This common language has a limit, and it stood silently throughout the whole topic. Maturity level was calculated on a date, a document was issued on a date, the control set was sampled for a period, the evidence file was compiled for one audit. All of it is a photograph that is correct once a year. Every day in between opens a new gap, a patch window slips, behavior corrected after training drifts back to its old state, and a recovery plan that was never rehearsed quietly goes stale. The photograph being correct says nothing about the 364 days between two photographs.

The next topic, Program Management, measures those 364 days: the gap between the speed of exposure inflow and remediation capacity, how a shrinking patch window trades off against rising outage caused by that same narrowing, which week an awareness metric drifts back to its old value, the plan item that fails to hold up in a drill, and the metric that never changes behavior at all.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close