Lesson 06 / 17
Security Frameworks
The structure of a function-based framework and a maturity calculation on a fictional control set: an overall maturity of 1.35 across 5 functions, 14 categories, and 31 control items; 5 controls sitting at level 0 and one category with no working control at all; 15 of 31 controls producing evidence for 25 evidence items; a 45 person-day budget where gap priority and yield priority rank all five functions differently; and a yield-first order that raises the average by 0.07 while leaving the weakest function 0.33 behind.
Published Updated
In this lesson
- Breaks a function-based framework into function, category, and control layers and defines the maturity level
- Calculates function maturity on a fictional control set and counts what the average hides: level-0 controls and a category with no working control
- Spends the same budget by gap priority and by yield priority and shows where the two orders diverge
Contents
The previous topic left behind a list: items were ranked, options were pitted against each other by cost, and the case for spending a scarce budget on one item over another was made with numbers. That list has a flaw invisible from inside the organization — it is written entirely in the organization’s own language. The organization chose the metrics, the organization assigned the weights, the organization produced the ranking. When an outsider — a buyer, an insurer, or another organization purchasing a service — says “show me your security posture,” this list is not an answer: there is no common axis on which the other party can lay it next to their own list.
A security framework is that axis. It is a common classification that breaks down the security work an organization does into countable headings, independent of who does it or with what tool. This lesson treats the framework by its structure and ties it to a single question: when the same control set is placed into this structure, what number comes out, what does that number show, and what does it hide. No framework, standard, or organization name appears anywhere in this lesson; what is measured is not a name but the structure itself.
Function, Category, Control
A function-based framework has three layers. At the top sits the function, which splits security work into five sequential headings: identify (what do I have, what risk am I carrying), protect (what narrows access), detect (how do I see what is happening), respond (what do I do once I see it), and recover (how do I get back on my feet afterward). Each function is split into categories, and each category into individual controls. The point of this breakdown is that the work two different organizations do with two different tools falls under the same five headings, so the two organizations become comparable in the same table.
The second layer is the maturity level, and it measures not whether a control exists but how institutionalized it is. This distinction is the lesson’s central concept: a control existing and a control operating in an auditable way are not the same thing. The model below uses five levels — nonexistent, performed without documentation, documented, measured, and continuously improved.
- FW1. The example organization is the fictional regional measurement network carried through nine courses: endpoints, network, identity, leased accounts, and three suppliers. The control set and levels are fictional.
- FW2. The framework consists of five functions, fourteen categories, and thirty-one control items.
- FW3. The maturity level ranges from 0 to 4: 0 nonexistent, 1 performed without documentation, 2 documented, 3 measured, 4 continuously improved.
- FW4. Category maturity is the unweighted average of its controls’ maturity, and function maturity is the average of its categories; overall maturity is the average of the five functions.
- FW5. Raising a control by one level costs
2 + 3 × current levelperson-days: climbing out of a low level is cheap, carrying a high level is expensive. - FW6. Evidence production depends on the level: from level 2 onward, one document is produced; from level 3 onward, two more measurement log entries are produced. Levels 0 and 1 produce no evidence item at all.
// framework/maturity.mjs — calculating the maturity level in a function-based framework. // MODEL: regional measurement network (fictional); the control set and levels 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(""); // function | category | maturity levels of the controls in the category (0-4) export const CATEGORY = row(` identify|asset inventory|3 2 1 identify|business context|2 2 identify|risk assessment|3 2 protect|access control|3 3 2 protect|data security|3 2 2 protect|maintenance and patching|2 1 detect|continuous monitoring|2 1 1 detect|event detection|1 0 respond|response plan|2 1 respond|notification|1 0 respond|mitigation|1 1 recover|recovery plan|1 0 recover|improvement|0 0 recover|communications|1`) .map(([fn, name, d]) => ({ fn, name, level: d.split(" ").map(Number) })); export const FUNCTION = ["identify", "protect", "detect", "respond", "recover"]; export const categoriesOf = (i) => CATEGORY.filter((k) => k.fn === i); export const catAvg = (k) => k.level.reduce((a, b) => a + b, 0) / k.level.length; export const fnAvg = (i) => categoriesOf(i).reduce((a, k) => a + catAvg(k), 0) / categoriesOf(i).length; // Raising a control by one level costs 2 + 3 x current level person-days (fictional). export const days = (d) => 2 + 3 * d; // Evidence: one more document from level 2 onward, two more measurement log entries from level 3 onward. export const evidence = (d) => (d >= 2 ? 1 : 0) + (d >= 3 ? 2 : 0); if (import.meta.url === `file://${process.argv[1]}`) { const control = CATEGORY.flatMap((k) => k.level); console.log(`model: ${FUNCTION.length} functions, ${CATEGORY.length} categories, ` + `${control.length} control items`); const y = gen([10, 9, 8, 10, 30, 9]); console.log("\n" + y("function", "category", "control", "maturity", "weakest", "level 0")); for (const i of FUNCTION) { const c = categoriesOf(i), d = c.flatMap((k) => k.level); const weakest = c.reduce((a, k) => (catAvg(k) < catAvg(a) ? k : a)); console.log(y(i, c.length, d.length, fnAvg(i).toFixed(2), `${weakest.name} ${catAvg(weakest).toFixed(1)}`, d.filter((x) => x === 0).length)); } const all = CATEGORY.flatMap((k) => k.level); const overallAvg = FUNCTION.reduce((a, i) => a + fnAvg(i), 0) / FUNCTION.length; const noWorking = CATEGORY.filter((k) => k.level.every((d) => d === 0)); console.log(`\noverall maturity ${overallAvg.toFixed(2)}; level 0 controls ` + `${all.filter((d) => d === 0).length}/${all.length}; ` + `categories with no working control ${noWorking.length} ` + `(${noWorking.map((k) => k.name).join(", ")})`); console.log(`controls producing evidence ${all.filter((d) => d >= 2).length}/${all.length}; ` + `evidence items produced ${all.reduce((a, d) => a + evidence(d), 0)}`); // Gain from raising one level toward function maturity: 1 / (categories x controls). const z = gen([10, 9, 14, 33, 11]); console.log("\n" + z("function", "maturity", "maturity rank", "cheapest +1", "yield rank")); const yieldList = FUNCTION.map((i) => { const c = categoriesOf(i); const best = c.flatMap((k) => k.level.map((d) => ({ name: k.name, gain: 1 / (c.length * k.level.length) / days(d), d }))) .reduce((a, b) => (b.gain > a.gain ? b : a)); return { i, avg: fnAvg(i), best }; }); const sM = [...yieldList].sort((a, b) => a.avg - b.avg).map((v) => v.i); const sY = [...yieldList].sort((a, b) => b.best.gain - a.best.gain).map((v) => v.i); for (const v of yieldList) { console.log(z(v.i, v.avg.toFixed(2), sM.indexOf(v.i) + 1, `${v.best.name} ${days(v.best.d)} days`, sY.indexOf(v.i) + 1)); } const same = sM.filter((i, k) => i === sY[k]).length; console.log(`\nmaturity rank: ${sM.join(" > ")}`); console.log(`yield rank : ${sY.join(" > ")}`); console.log(`functions in the same rank: ${same}/${FUNCTION.length}`); }
model: 5 functions, 14 categories, 31 control items function category control maturity weakest level 0 identify 3 7 2.17 asset inventory 2.0 0 protect 3 8 2.17 maintenance and patching 1.5 0 detect 2 5 0.92 event detection 0.5 1 respond 3 6 1.00 notification 0.5 1 recover 3 5 0.50 improvement 0.0 3 overall maturity 1.35; level 0 controls 5/31; categories with no working control 1 (improvement) controls producing evidence 15/31; evidence items produced 25 function maturity maturity rank cheapest +1 yield rank identify 2.17 4 asset inventory 5 days 5 protect 2.17 5 maintenance and patching 5 days 4 detect 0.92 2 event detection 2 days 1 respond 1.00 3 notification 2 days 2 recover 0.50 1 recovery plan 2 days 3 maturity rank: recover > detect > respond > identify > protect yield rank : detect > respond > recover > protect > identify functions in the same rank: 0/5
The first table does the framework’s real job: thirty-one scattered control items collapse into five numbers, and the five numbers are comparable. The organization’s distribution is skewed — protect and identify sit at 2.17, recover at 0.50. This is exactly what nine courses have left behind: a great deal of work has gone into narrowing access, changing what data shows when it is read, and drawing the boundary, and very little work into getting back on your feet after an event. This skew is only visible once the five headings are written out separately.
Overall maturity is 1.35, and this number is misleading when it stands alone. For two reasons: first, the average swallows the weakest link — the recover function sits at about a third of the average, while the overall number lands somewhere in the middle. Second, the average does not distinguish between a control that is entirely absent and one that is present at a low level. The last column makes that distinction: five of the thirty-one controls are at level 0, and one category — improvement — has no working control at all. That single line produces a different decision than the sentence “our recovery maturity is 0.50,” because a category being completely empty is not a slowdown, it is a gap.
The third number opens up the evidence side and feeds the next two lessons. Fifteen of the thirty-one controls produce evidence, and twenty-five evidence items are produced in total. The sixteen controls at level 0 and 1, on the other hand, can show nothing in an audit: the work may be happening, but there is no item that demonstrates it happened. The maturity level being so tightly bound to evidence is no accident — the level definitions themselves are written in terms of documents and measurement log entries.
The second table carries the course’s measurement axis into the framework. The left side ranks the functions by maturity: recover, the weakest, comes first. The right side ranks the same functions by maturity gain per unit of effort. The two rankings do not agree on a single function. The reason is structural: a contribution to a function’s maturity is divided by that function’s number of categories and the number of controls in the category. Detect has two categories, so one upgrade there moves the needle more than one upgrade in recover — which is why recover, the lowest function, ranks third in the yield order.
The Same Budget, Two Orders
The real difference between the two rankings shows up once a fixed budget is spent. The file below raises the same control set under two separate rules: gap first takes, at every step, the cheapest item belonging to the lowest-maturity function; yield first takes the item that contributes the most to overall maturity per day. The budget is forty-five person-days and is fictional.
// framework/budget.mjs — the same framework raised by two different orders (fictional budget). import { CATEGORY, FUNCTION, gen, days, evidence } from "./maturity.mjs"; const BUDGET = 45; // person-days (fictional unit) const clone = () => CATEGORY.map((k) => ({ ...k, level: [...k.level] })); const avg = (K, i) => { const c = K.filter((k) => k.fn === i); return c.reduce((a, k) => a + k.level.reduce((x, d) => x + d, 0) / k.level.length, 0) / c.length; }; const overall = (K) => FUNCTION.reduce((a, i) => a + avg(K, i), 0) / FUNCTION.length; const evidenceCount = (K) => K.flatMap((k) => k.level).reduce((a, d) => a + evidence(d), 0); const zeroCount = (K) => K.flatMap((k) => k.level).filter((d) => d === 0).length; // Upgrade candidate: category, control, cost, and gain toward overall maturity. const candidates = (K) => K.flatMap((k, ki) => k.level.map((d, di) => ({ ki, di, d, k, cost: days(d), gain: 1 / (FUNCTION.length * K.filter((x) => x.fn === k.fn).length * k.level.length) })).filter((a) => a.d < 4)); function run(pick) { const K = clone(); let left = BUDGET, steps = 0; for (;;) { const eligible = candidates(K).filter((a) => a.cost <= left); if (!eligible.length) break; const a = pick(eligible, K); K[a.ki].level[a.di]++; left -= a.cost; steps++; } return { K, left, steps }; } const ROUTE = [ ["gap first", (u, K) => { const lowest = [...FUNCTION].sort((a, b) => avg(K, a) - avg(K, b))[0]; const within = u.filter((a) => a.k.fn === lowest); return (within.length ? within : u).sort((a, b) => a.cost - b.cost || a.ki - b.ki)[0]; }], ["yield first", (u) => [...u].sort((a, b) => b.gain / b.cost - a.gain / a.cost || a.ki - b.ki)[0]], ]; const y = gen([13, 9, 7, 8, 9, 9, 17]); console.log(`budget ${BUDGET} person-days; starting overall maturity ` + `${overall(clone()).toFixed(2)}, level 0 controls ${zeroCount(clone())}, ` + `evidence items ${evidenceCount(clone())}`); console.log("\n" + y("run", "raised", "left", "overall", "level 0", "evidence", "weakest function")); const result = {}; for (const [name, pick] of ROUTE) { const { K, left, steps } = run(pick); const weakest = [...FUNCTION].sort((a, b) => avg(K, a) - avg(K, b))[0]; result[name] = { K, weakest }; console.log(y(name, steps, left, overall(K).toFixed(2), zeroCount(K), evidenceCount(K), `${weakest} ${avg(K, weakest).toFixed(2)}`)); } const z = gen([11, 12, 14, 14, 12]); console.log("\n" + z("function", "baseline", "gap first", "yield first", "diff")); for (const i of FUNCTION) { const a = avg(result["gap first"].K, i), b = avg(result["yield first"].K, i); console.log(z(i, avg(clone(), i).toFixed(2), a.toFixed(2), b.toFixed(2), (b - a).toFixed(2))); } const v = result["yield first"].K; const untouched = FUNCTION.filter((i) => avg(v, i) === avg(clone(), i)); console.log(`\nyield first never raised ${untouched.length} function(s): ` + `${untouched.join(", ")}`); console.log(`overall maturity difference ${(overall(v) - overall(result["gap first"].K)).toFixed(2)}; ` + `weakest function difference ${(avg(v, result["yield first"].weakest) - avg(result["gap first"].K, result["gap first"].weakest)).toFixed(2)}`);
budget 45 person-days; starting overall maturity 1.35, level 0 controls 5, evidence items 25 run raised left overall level 0 evidence weakest function gap first 12 0 1.77 0 32 detect 1.50 yield first 11 2 1.83 0 32 respond 1.17 function baseline gap first yield first diff identify 2.17 2.17 2.17 0.00 protect 2.17 2.17 2.33 0.17 detect 0.92 1.50 1.83 0.33 respond 1.00 1.50 1.17 -0.33 recover 0.50 1.50 1.67 0.17 yield first never raised 1 function(s): identify overall maturity difference 0.07; weakest function difference -0.33
Both rows spend the same forty-five days, and both bring the five level-0 controls up out of zero; evidence items rise from twenty-five to thirty-two in both. Where they diverge is the last two columns. The yield order carries overall maturity to 1.83, while the gap order stays at 1.77 — a difference of 0.07, and read on its own, the yield order looks like the winner. But look at the weakest function and the picture reverses: the gap order brings the weakest function up to 1.50, the yield order leaves it at 1.17. The difference is 0.33 — roughly five times what was gained on average piles up as a loss in the weakest link.
The table below shows why. The yield order leaves the respond function 0.33 behind the gap order and never touches the identify function at all. The rule does this knowingly: an upgrade in identify costs five days and moves little, one in detect costs two days and moves a lot. The yield measure does not ask how critical an item is, only how much it moves the metric. For this lesson, the conclusion is structural: once a maturity number becomes a target, the order that raises the number most cheaply and the order that closes the gap diverge, and the same budget produces two different organizations.
A third observation follows from this, and it concerns the framework itself. The unweighted average (FW4) is a choice, not an inevitability: a function with fewer categories moves more per item. The rule that constructs the metric also decides which item the metric favors. If a framework report shows only five numbers, how those five numbers were assembled must also be asked.
Summary
- A function-based framework has three layers — function, category, control — and reduces thirty-one scattered items to five comparable numbers: identify and protect at 2.17, respond at 1.00, detect at 0.92, recover at 0.50.
- Overall maturity is 1.35 and hides two things: it swallows the weakest function, and it does not distinguish “absent” from “low.” Five of the thirty-one controls are at level 0, and one category has no working control at all.
- Evidence production depends on the level: only fifteen of the thirty-one controls produce evidence, yielding twenty-five items in total; the sixteen controls at level 0 and 1 can show nothing in an audit.
- Ranking the functions by maturity and ranking them by gain per unit of effort do not agree on a single one of the five functions; recover, the lowest, ranks third in the yield order.
- On a forty-five person-day budget, the yield order raises overall maturity by an extra 0.07 but leaves the weakest function 0.33 behind and never touches one function at all: raising the number and closing the gap are not the same job.
Next Step
The framework has established a common axis and reduced scattered controls to comparable numbers. All of these numbers share one property: the organization itself calculated them. The organization assigned the levels, the organization took the average, the organization decided which item to raise. For an outsider, this is only one step ahead of the list from the previous topic — the language became shared, but the party producing the number did not change.
The next lesson takes up the structure that takes that step: a management system model with a declared scope, a written record of which control is applied and why or why not, an internal audit, and certification by an outside party. What gets measured is not the certificate itself but the scope statement: narrowing the scope makes the certificate cheaper, and, by the same move, leaves a certain number of items outside it.
To keep your progress and take notes, Log in
My notes
Log in to take notes.