Lesson 04 / 11
Security Champions
The distribution of security competence is measured over five teams' 35 weekly reviews: the central team alone covers 16 reviews and 19 go through without a security eye, and coverage saturates with four champions. A single champion covers an average of 0.86 teams, and even with a champion on every team, 6 reviews a week are left to the center; when a fifth champion is added, catching items that require expertise drops from 2 to 1, because the center no longer sees those reviews.
Published Updated
In this lesson
- Measures the distribution of security competence across teams as a count of covered and uncovered reviews
- Compares a champion's capacity against team load: calculates how many teams it covers and how many reviews are left to the center
- Shows how depth falls as coverage grows, and counts the items an escalation path gains
Contents
The previous three lessons, in turn, spread checks across stages, computed the cost per stage, and turned requirements into a testable form. All three called on a person but never gave that person a name: the one who notices an unobservable predicate in a design session, the one who separates the six real items out of static analysis’s ninety warnings, the one who runs the abuse case. This person was assumed to be ready and waiting on every team.
This lesson measures that assumption. A security champion is a developer who works inside their own team and sets aside part of their time for security review; not an alternative to a central security team, but an extension of its capacity. There are three things to measure: how many reviews have a security eye, how many are waiting, and how many teams a champion’s capacity covers.
- DA17. The teams, developer counts, and weekly review counts are fictional; the total review load is consistent with the previous lessons’ change flow.
- DA18. A champion only looks at their own team’s reviews; context knowledge is a resource that cannot be transferred. A central specialist can look at any team, and their capacity sits in a shared pool.
- DA19. Item difficulty has three levels: 1 is seen with a checklist, 2 with a trained eye, 3 with expertise. A champion catches at most level 2; level 3 is the central specialist’s job.
- DA20. An item is only caught in a review that has a security eye. An item that passes through an uncovered review falls, in the previous lessons’ language, into the late flow.
- DA21. The escalation rate is the share of cases where a champion still forwards an item they cannot see to the center, and it is a model value. When it is zero, the champion model’s depth ceiling becomes visible.
Covered and Uncovered Reviews
The model ranks teams by review load and separates two sources of capacity: the time champions spend within their own teams, and the central team’s shared pool.
// team.mjs — five teams, review load, and the distribution of security competence (model) // row: team | developer | weekly review export const TEAM = ` subscriber-portal 6 10 meter-collection 5 8 billing 4 6 field-app 4 5 shared-library 3 6` .trim().split('\n').map((r) => { const [name, g, i] = r.trim().split(/\s+/); return { name, developer: +g, review: +i }; }); export const CHAMPION_SHARE = 0.2; // the share a champion sets aside from their own time export const CHAMPION_REVIEWS = 6; // the number of reviews they can handle per week with that share export const CENTRAL_PEOPLE = 2, CENTRAL_REVIEWS = 8; // central security team // Item difficulty: 1 is seen with a checklist, 2 with a trained eye, 3 with expertise. // The distribution is modeled on the previous lessons' 23-item set. export const DIFFICULTY = [[1, 11], [2, 7], [3, 5]]; export const CHAMPION_DEPTH = 2; // the highest difficulty a champion can catch export const TOTAL_REVIEWS = TEAM.reduce((s, e) => s + e.review, 0); export const TOTAL_DEVELOPERS = TEAM.reduce((s, e) => s + e.developer, 0);
// run1.mjs — as the champion count rises: reviews with a security eye and reviews left waiting import { TEAM, CHAMPION_SHARE, CHAMPION_REVIEWS, CENTRAL_PEOPLE, CENTRAL_REVIEWS, TOTAL_REVIEWS, TOTAL_DEVELOPERS } from './team.mjs'; const central = CENTRAL_PEOPLE * CENTRAL_REVIEWS; // A champion only looks at their own team's reviews; the central pool can look at any team. const sorted = [...TEAM].sort((a, b) => b.review - a.review); console.log(`${TEAM.length} teams, ${TOTAL_DEVELOPERS} developers, ` + `${TOTAL_REVIEWS} reviews/week; central ${central} reviews/week`); console.log('\nchampion by champions central used central idle covered waiting person-equiv'); for (let s = 0; s <= TEAM.length; s++) { const coverage = sorted.slice(0, s).reduce((a, e) => a + Math.min(e.review, CHAMPION_REVIEWS), 0); const remaining = TOTAL_REVIEWS - coverage, used = Math.min(remaining, central); const personEquiv = (s * CHAMPION_SHARE + CENTRAL_PEOPLE).toFixed(1); console.log(String(s).padEnd(10) + String(coverage).padEnd(18) + String(used).padEnd(19) + String(central - used).padEnd(14) + String(coverage + used).padEnd(10) + String(remaining - used).padEnd(10) + personEquiv); } console.log('\ndoes a single champion cover their own team'); console.log('team review champion capacity gap % left to central'); for (const e of sorted) console.log(e.name.padEnd(18) + String(e.review).padEnd(10) + String(CHAMPION_REVIEWS).padEnd(21) + String(Math.max(0, e.review - CHAMPION_REVIEWS)).padEnd(6) + ((Math.max(0, e.review - CHAMPION_REVIEWS) / e.review) * 100).toFixed(0)); const gap = sorted.reduce((a, e) => a + Math.max(0, e.review - CHAMPION_REVIEWS), 0); console.log(`even with a champion on every team, ${gap} reviews/week are left to the center; ` + `a single champion covers an average of ${(CHAMPION_REVIEWS / (TOTAL_REVIEWS / TEAM.length)).toFixed(2)} teams`);
5 teams, 22 developers, 35 reviews/week; central 16 reviews/week champion by champions central used central idle covered waiting person-equiv 0 0 16 0 16 19 2.0 1 6 16 0 22 13 2.2 2 12 16 0 28 7 2.4 3 18 16 0 34 1 2.6 4 24 11 5 35 0 2.8 5 29 6 10 35 0 3.0 does a single champion cover their own team team review champion capacity gap % left to central subscriber-portal 10 6 4 40 meter-collection 8 6 2 25 billing 6 6 0 0 shared-library 6 6 0 0 field-app 5 6 0 0 even with a champion on every team, 6 reviews/week are left to the center; a single champion covers an average of 0.86 teams
Row zero gives the starting condition: the two-person central team can handle 16 reviews a week, and 19 reviews go through without a security eye. The covered share is 46 percent, and that number has nothing to do with the central team’s diligence — 16 units of capacity have been allocated to 35 reviews.
As champions are added, the covered count rises from 16 to 22, to 28, to 34, and saturates at the fourth champion. The fifth champion does not change the covered count at all; it only raises the center’s idle capacity from 5 to 10. The person-equivalent column gives the cost: with four champions, total security competence adds up to 2.8 full-time people, and 0.8 of that comes out of the twenty-two existing developers. Getting the same coverage by growing the central team would require enough specialists to handle 35 reviews; the champion model is a way to saturate coverage without hiring anyone new.
How Many Teams Does One Champion Cover
The second table disproves a common design assumption with a number. The sentence “one champion per team” implies that coverage is complete; the numbers do not say that. A champion’s weekly capacity is six reviews, the average team load is seven reviews — meaning a single champion covers an average of 0.86 teams.
The distribution is sharper than the average. The subscriber portal produces ten reviews a week; the champion handles six of them, four are left to the center, and that is 40 percent of that team’s reviews. For the meter collection service the share is 25 percent. The three smaller teams have a zero gap, and the field app’s champion has one review’s worth of idle capacity, but that capacity cannot be moved to another team — context does not travel.
The result is this: even with a champion on every team, six reviews a week are left to the center. The champion count saturates coverage at the team boundary, not organization-wide; a central capacity to cover the remaining load is needed in every case. And if capacity is not distributed according to team load, the team that produces the most reviews carries the biggest gap — that is the column the table is sorted by.
Depth Does Not Grow With Coverage
A covered review count is not the same thing as a caught item count. Who sits in a review determines which difficulty of item gets seen.
// run2.mjs — what happens to depth as coverage grows: whose eyes do difficulty-3 items pass through import { TEAM, CHAMPION_REVIEWS, CENTRAL_PEOPLE, CENTRAL_REVIEWS, DIFFICULTY, CHAMPION_DEPTH, TOTAL_REVIEWS } from './team.mjs'; const central = CENTRAL_PEOPLE * CENTRAL_REVIEWS; const sorted = [...TEAM].sort((a, b) => b.review - a.review); const shallow = DIFFICULTY.filter(([z]) => z <= CHAMPION_DEPTH).reduce((a, [, n]) => a + n, 0); const deep = DIFFICULTY.filter(([z]) => z > CHAMPION_DEPTH).reduce((a, [, n]) => a + n, 0); const ESCALATION = 0.6; // the share of deep items the champion flags as "not sure" and forwards to the center console.log(`${shallow + deep} items: ${shallow} visible to a champion, ${deep} requiring expertise`); console.log('\nchampion by champions by central shallow found deep found + escalation total'); for (let s = 0; s <= TEAM.length; s++) { const coverage = sorted.slice(0, s).reduce((a, e) => a + Math.min(e.review, CHAMPION_REVIEWS), 0); const centralCoverage = Math.min(TOTAL_REVIEWS - coverage, central); const shallowCaught = Math.round((shallow * (coverage + centralCoverage)) / TOTAL_REVIEWS); const deepCaught = Math.round((deep * centralCoverage) / TOTAL_REVIEWS); const withEscalation = Math.round((deep * (centralCoverage + ESCALATION * coverage)) / TOTAL_REVIEWS); console.log(String(s).padEnd(10) + String(coverage).padEnd(18) + String(centralCoverage).padEnd(16) + String(shallowCaught).padEnd(15) + String(deepCaught).padEnd(17) + String(withEscalation).padEnd(14) + (shallowCaught + withEscalation)); } console.log(`\nescalation's extra load on the center at 5 champions is ` + `${(deep * ESCALATION * 29 / TOTAL_REVIEWS).toFixed(1)} items; ` + `the center's idle capacity is ${central - 6} reviews/week`);
23 items: 18 visible to a champion, 5 requiring expertise champion by champions by central shallow found deep found + escalation total 0 0 16 8 2 2 10 1 6 16 11 2 3 14 2 12 16 14 2 3 17 3 18 16 17 2 4 21 4 24 11 18 2 4 22 5 29 6 18 1 3 21 escalation's extra load on the center at 5 champions is 2.5 items; the center's idle capacity is 10 reviews/week
The shallow-item column behaves as expected: it rises from eight to eighteen and completes at the fourth champion. The deep-item column does not behave. The central team alone catches two deep items, and that number does not change at all until the champion count reaches four; at the fifth champion it drops to one.
The reason for the drop sits in the neighboring column. When champions cover 29 reviews, only 6 reviews are left to the center; as the number of reviews the center looks at falls, so does the odds of it seeing an item that requires expertise. The champion is sitting in those reviews but is not at the level to see a deep item. The decision that grew coverage shrank depth. The total column gives this in a single number: the best result is 22 items at four champions, 21 items at five.
The last column measures the countermeasure. If there is an escalation path through which a champion can still forward an item they cannot see to the center, the deep-item count at five champions rises from 1 to 3. The cost of this is the 2.5-item extra load it brings to the center, and the center’s 10 reviews/week of idle capacity covers it. Whether the champion model works depends not on distributing competence but on being able to report insufficiency: a champion who can say “I’m not sure” gets more items caught than a champion trying to be sure.
A limitation has to be stated. The model holds the champion’s depth ceiling fixed; in reality the ceiling rises with training, and that is outside this measurement. What is measured is how coverage behaves at a given ceiling; training’s contribution is a separate decision with its own cost.
What Happens to an Uncovered Review
What happens to an uncovered review is not a question of capacity, it is a question of rule. If the security review is mandatory, an uncovered change waits; if it is not mandatory, it goes through and the item it carries slips past.
// run3.mjs — an uncovered review can be tied to two different rules: blocking and non-blocking import { TEAM, CHAMPION_REVIEWS, CENTRAL_PEOPLE, CENTRAL_REVIEWS, DIFFICULTY, CHAMPION_DEPTH, TOTAL_REVIEWS } from './team.mjs'; const WEEKS = 12, central = CENTRAL_PEOPLE * CENTRAL_REVIEWS; const sorted = [...TEAM].sort((a, b) => b.review - a.review); const shallow = DIFFICULTY.filter(([z]) => z <= CHAMPION_DEPTH).reduce((a, [, n]) => a + n, 0); const deep = DIFFICULTY.filter(([z]) => z > CHAMPION_DEPTH).reduce((a, [, n]) => a + n, 0); console.log(' gap blocking rule non-blocking rule'); console.log('champions /week changes waiting slip waiting items slipped'); for (let s = 0; s <= TEAM.length; s++) { const coverage = sorted.slice(0, s).reduce((a, e) => a + Math.min(e.review, CHAMPION_REVIEWS), 0); const centralCoverage = Math.min(TOTAL_REVIEWS - coverage, central), gap = TOTAL_REVIEWS - coverage - centralCoverage; const caught = Math.round((shallow * (coverage + centralCoverage)) / TOTAL_REVIEWS) + Math.round((deep * (centralCoverage + 0.6 * coverage)) / TOTAL_REVIEWS); console.log(String(s).padEnd(11) + String(gap).padEnd(8) + String(gap * WEEKS).padEnd(21) + '0'.padEnd(7) + '0'.padEnd(10) + (shallow + deep - caught)); } console.log('\nunder the blocking rule no item slips through, and up to 228 changes wait over 12 weeks;'); console.log('under the non-blocking rule no change waits, and up to 13 items go through unseen.');
gap blocking rule non-blocking rule champions /week changes waiting slip waiting items slipped 0 19 228 0 0 13 1 13 156 0 0 9 2 7 84 0 0 6 3 1 12 0 0 2 4 0 0 0 0 1 5 0 0 0 0 2 under the blocking rule no item slips through, and up to 228 changes wait over 12 weeks; under the non-blocking rule no change waits, and up to 13 items go through unseen.
The same gap count produces two completely different outcomes. With competence not distributed at all, the blocking rule holds back 228 changes over twelve weeks and slips no item through; the non-blocking rule holds back no change at all, and 13 items pass through without a security eye. This is the same trade-off as the first lesson’s gate table, but this time the reason for the hold is not the gate’s capacity, it is the absence of competence to review.
The direction of the decision depends on the champion count. At zero and one champion, the blocking rule cannot actually be run — the 228 and 156 accumulated changes stop the teams, the rule turns into an exception list within a few weeks, and it effectively degrades into the non-blocking rule. After the third champion, the gap falls to one review a week; it holds back 12 changes over twelve weeks, and the blocking rule becomes workable. A blocking rule cannot be written before competence is distributed; if it is written anyway, it will not be enforced, and an unenforced rule leaves even the reviews it was supposed to cover uncovered.
The last row shows the previous table’s finding once more. At the fifth champion the gap is zero, the waiting-changes count is zero, and everything looks covered; and yet the slipped-item count still rises from 1 at four champions to 2. A measurement that only looks at the coverage column never sees this increase.
Summary
- 5 teams produce 35 reviews a week; the two-person central team covers 16 of them, and 19 reviews go through without a security eye.
- As champions are added, covered reviews rise from 16 to 34 and saturate at 35 with the fourth champion; the fifth champion does not change coverage, it only raises the center’s idle capacity from 5 to 10.
- A champion’s capacity is 6 reviews, the average team load is 7 reviews: a single champion covers 0.86 teams. Even with a champion on every team, 6 reviews a week are left to the center, and 4 of those come from a single team.
- A four-champion arrangement costs 2.8 person-equivalents, and 0.8 of that comes out of the existing 22 developers; coverage saturates without hiring anyone new. For uncovered reviews, the blocking rule holds back 228 changes over 12 weeks and slips no item through, while the non-blocking rule holds back nothing and slips 13 items through; the blocking rule only becomes workable after the third champion.
- Catching the 5 items that require expertise does not rise with the champion count: it stays flat at 2 up to four champions, and drops to 1 at the fifth, because the center no longer sees those reviews.
- An escalation path raises the deep-item count at five champions from 1 to 3; its cost is a 2.5-item extra load on the center, and the center’s 10 reviews of idle capacity cover it.
Next Step
This lesson measured where to place competence and found that a four-champion arrangement catches 22 of the 23 items. Being caught does not close an item. Every previous lesson assumed that a found item gets fixed; even shift left’s 250-hour gain stood on top of that assumption, and it was noted there that a ceiling exists. The same twenty-two developers are the ones who will fix a found item, and their time is shared with feature work. The next lesson distributes limited development capacity across the item set: how many items a sprint’s capacity takes, how many items close per unit of capacity, how many items are left out when the ranking criterion changes, and how much the set of items left out changes when the criterion changes.
To keep your progress and take notes, Log in
My notes
Log in to take notes.