Lesson 02 / 16
Patch Management
The patch's measure is taken as the window: the days between announcement and application for 12 announcements across a fleet of 102 machines, the point-day equivalent of the 1038 foothold points left open in that window, the 939 unplanned outages the emergency path produces by skipping 36 of 48 test items, and the 12 points that never close under any regime.
Published Updated
In this lesson
- Shows that a patch does not change the foothold point count, and names what it closes as the known path
- Measures the window between announcement and application per machine class and totals it as point-days
- Reads the window the emergency path shortens together with the test items it skips and the outage count it produces
Contents
The previous lesson left 44 foothold points unclosed across four machine classes: the work itself requires them. A point that cannot be closed does not age where it stands, but the component that defines it does. When a flaw is announced in the binary behind a service definition, that point — because it cannot be removed — stays reachable by a known path until the patch is applied.
This lesson’s measure is the window: the interval between the day an announcement is published and the day the patch is applied. On its own, the window is only a span of time; the weight it carries emerges once it is multiplied by the number of points left open in that interval. The unit is the point-day.
What a Patch Closes
This lesson begins with a distinction, because without it every number that follows gets written to the wrong column. A patch does not remove a foothold point. After the patch is applied, the service definition stays in place, the scheduled task stays in place, the boot entry stays in place. The only thing that changes is that a known flaw allowing the point’s definition to be written disappears.
For the course’s two columns, this means: the number of points a patch closes is zero. What a patch reduces is the duration for which the point is reachable. The previous lesson’s pruning actually closed 40 points; a patch closes none of them, it only determines how long the remaining 44 stay open. Neither job substitutes for the other, and neither makes the other unnecessary.
The post-install exposed surface and the four parts of the patch window were measured in the Server, Network, and Hardening course; they are not repeated here. This lesson’s measure is the foothold point, not the server item, and the count is taken across the fleet.
- SH7. The fleet of 102 machines, four classes, and 12 announcements are a model; no real patch is downloaded, no real fleet is scanned. The remaining-point count per class (9, 12, 12, 11) comes from the previous lesson.
- SH8. Each foothold point’s definition is counted as belonging to a single component. In reality a point can depend on more than one component; that would only increase the counts, not change their direction.
- SH9. There is no randomness: the announcement days, the components, and the calendar are fixed in the model.
- SH10. Flawed patches and rollbacks are not modeled; both were measured in the previous course and are taken here as input.
- SH11. The maintenance calendar has a fixed interval. The number of machines that miss a turn because they are offline is fixed per class, and those machines are patched on the next turn.
- SH12. The emergency path applies in 1.5 days and skips three of the four test items; machines that miss their turn also miss the emergency path.
// patch/model.mjs — the window IS A MODEL: the announcement day, the application day, and the // open foothold point in that interval, in a constructed fleet. No real patch is downloaded, no real fleet is scanned. // The remaining-point count per class (9, 12, 12, 11) comes from the previous lesson. export const CLASS = [ // name, machines, calendar interval (days), calendar offset, machines that miss a turn because they stay offline { name: 'field-laptop', machines: 60, calendar: 21, offset: 6, missed: 9 }, { name: 'branch-endpoint', machines: 24, calendar: 14, offset: 3, missed: 1 }, { name: 'collector', machines: 12, calendar: 7, offset: 2, missed: 0 }, { name: 'processing-server', machines: 6, calendar: 30, offset: 9, missed: 0 }, ]; // Each component carries the definition for how many foothold points per class; heavy: the machine restarts. export const COMPONENT = { kernel: { points: [2, 2, 2, 2], heavy: true }, 'system-library': { points: [1, 1, 1, 1], heavy: true }, 'remote-management': { points: [0, 1, 1, 1], heavy: false }, 'monitoring-agent': { points: [2, 2, 2, 2], heavy: false }, 'patch-agent': { points: [2, 2, 2, 2], heavy: false }, 'identity-plugin': { points: [0, 1, 1, 1], heavy: false }, shell: { points: [2, 2, 2, 1], heavy: false }, 'business-application': { points: [0, 1, 1, 1], heavy: false }, }; export const ANNOUNCEMENT = [ // [day, component, emergency?] [4.0, 'system-library', false], [13.5, 'kernel', true], [21.0, 'shell', false], [28.5, 'monitoring-agent', false], [34.0, 'remote-management', true], [42.5, 'system-library', false], [51.5, 'patch-agent', false], [58.5, 'kernel', false], [66.0, 'business-application', false], [74.5, 'identity-plugin', true], [88.0, 'shell', false], [97.5, 'system-library', true], ]; export const HORIZON = 120, EMERGENCY_DURATION = 1.5, TEST_ITEM = 4, EMERGENCY_SKIPPED = 3; // Unpatchable item: the business application cannot be upgraded because of the collection service's version compatibility. export const UNPATCHABLE = [['business-application', 'collector']]; export const patchable = (b, i) => !UNPATCHABLE.some(([x, y]) => x === b && y === CLASS[i].name); export const calendarDay = (i, g) => { const s = CLASS[i]; return Math.ceil((g - s.offset) / s.calendar) * s.calendar + s.offset; }; export const TOTAL_POINTS = CLASS.reduce((a, s, i) => a + s.machines * Object.values(COMPONENT).reduce((b, c) => b + c.points[i], 0), 0); // The window and point-days of an announcement in a class. regime: 'calendar' | 'mixed' | 'all-emergency' export function window(d, i, regime) { const [g, b, emergency] = d, n = COMPONENT[b].points[i], s = CLASS[i]; const empty = { points: 0, pointDays: 0, openPointDays: 0, outage: 0, openRemaining: 0 }; if (n === 0) return empty; if (!patchable(b, i)) return { ...empty, points: n * s.machines, openPointDays: n * s.machines * (HORIZON - g), openRemaining: n * s.machines }; const emergencyPath = regime === 'all-emergency' || (regime === 'mixed' && emergency); const first = emergencyPath ? g + EMERGENCY_DURATION : calendarDay(i, g); const next = calendarDay(i, first + 0.001); // machines that miss the first turn const onTime = s.machines - s.missed; return { ...empty, points: n * s.machines, pointDays: n * (onTime * (first - g) + s.missed * (next - g)), outage: emergencyPath ? onTime : 0 }; } export function scan(regime) { let pointDays = 0, weight = 0, full = 0, service = 0, open = 0, openDays = 0, skipped = 0; for (const d of ANNOUNCEMENT) { const emergencyPath = regime === 'all-emergency' || (regime === 'mixed' && d[2]); if (emergencyPath) skipped += EMERGENCY_SKIPPED; for (let i = 0; i < CLASS.length; i += 1) { const p = window(d, i, regime); pointDays += p.pointDays; openDays += p.openPointDays; open += p.openRemaining; if (p.openRemaining === 0) weight += p.points; if (COMPONENT[d[1]].heavy) full += p.outage; else service += p.outage; } } return { avgWindow: pointDays / weight, pointDays, full, service, open, openDays, skipped }; }
Window per Announcement
// patch/measure.mjs — the window, the open point in the window, and the cost of an emergency patch import { CLASS, COMPONENT, ANNOUNCEMENT, HORIZON, EMERGENCY_DURATION, TEST_ITEM, TOTAL_POINTS, UNPATCHABLE, window, calendarDay, scan } from './model.mjs'; const row = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join('').trimEnd()); const f1 = (x) => x.toFixed(1); console.log(`${CLASS.reduce((a, s) => a + s.machines, 0)} machines, ${TOTAL_POINTS} foothold points, ` + `${ANNOUNCEMENT.length} announcements over ${HORIZON} days (${ANNOUNCEMENT.filter((d) => d[2]).length} marked emergency)`); const A = [-24, 6, 8, 8, 11, 9, 12, 12]; console.log('\n1. window (days) and open point per announcement, calendar regime'); row(A, 'announcement', 'day', 'field', 'branch', 'collector', 'server', 'open point', 'point-days'); for (const d of ANNOUNCEMENT) { const p = CLASS.map((_, i) => window(d, i, 'calendar')); row(A, d[1], f1(d[0]), ...CLASS.map((s, i) => (COMPONENT[d[1]].points[i] === 0 ? '-' : f1(calendarDay(i, d[0]) - d[0]))), p.reduce((a, x) => a + x.points, 0), p.reduce((a, x) => a + x.pointDays + x.openPointDays, 0)); } const B = [-16, 14, 13, 10, 13, 14, 12]; console.log('\n2. three regimes: window, point-days, and the cost of an emergency patch'); row(B, 'regime', 'avg window', 'point-days', 'skipped', 'full outage', 'service out.', 'open point'); for (const regime of ['calendar', 'mixed', 'all-emergency']) { const t = scan(regime); row(B, regime, f1(t.avgWindow), t.pointDays, `${t.skipped}/${TEST_ITEM * ANNOUNCEMENT.length}`, t.full, t.service, t.open); } console.log("\n3. the patch's effect on point count"); const t = scan('mixed'); row([-50, 12], 'foothold points before the patch', TOTAL_POINTS); row([-50, 12], 'points the patch closes', 0); row([-50, 12], 'foothold points after the patch', TOTAL_POINTS); row([-50, 12], 'point-days closed by the known path', t.pointDays); for (const [b, cls] of UNPATCHABLE) { const i = CLASS.findIndex((s) => s.name === cls), d = ANNOUNCEMENT.find((x) => x[1] === b); row([-50, 12], `unpatchable ${b}/${cls}`, `${COMPONENT[b].points[i] * CLASS[i].machines} points x ${f1(HORIZON - d[0])} days`); } row([-50, 12], 'average window shortened by the emergency path', f1(scan('calendar').avgWindow - scan('all-emergency').avgWindow)); row([-50, 12], "emergency path's application time", `${f1(EMERGENCY_DURATION)} days`);
102 machines, 1038 foothold points, 12 announcements over 120 days (4 marked emergency) 1. window (days) and open point per announcement, calendar regime announcement day field branch collector server open point point-days system-library 4.0 2.0 13.0 5.0 5.0 102 725 kernel 13.5 13.5 3.5 2.5 25.5 204 2560 shell 21.0 6.0 10.0 2.0 18.0 198 1762 monitoring-agent 28.5 19.5 2.5 1.5 10.5 204 3028 remote-management 34.0 - 11.0 3.0 5.0 42 344 system-library 42.5 5.5 2.5 1.5 26.5 102 770 patch-agent 51.5 17.5 7.5 6.5 17.5 204 3232 kernel 58.5 10.5 0.5 6.5 10.5 204 1972 business-application 66.0 - 7.0 6.0 3.0 42 848 identity-plugin 74.5 - 12.5 4.5 24.5 42 515 shell 88.0 2.0 13.0 5.0 11.0 198 1456 system-library 97.5 13.5 3.5 2.5 1.5 102 1136 2. three regimes: window, point-days, and the cost of an emergency patch regime avg window point-days skipped full outage service out. open point calendar 10.8 17700 0/48 0 0 12 mixed 8.6 14080.5 12/48 184 82 12 all-emergency 2.3 3801 36/48 460 479 12 3. the patch's effect on point count foothold points before the patch 1038 points the patch closes 0 foothold points after the patch 1038 point-days closed by the known path 14080.5 unpatchable business-application/collector 12 points x 54.0 days average window shortened by the emergency path 8.5 emergency path's application time 1.5 days
The Window Is a Product of the Calendar, Not the Component
The first table’s rows show the same announcement as four separate windows. For the shell announcement on day twenty-one, the window is 6 days on the field laptop, 10 on the branch endpoint, 2 on the collector, and 18 on the processing server. The difference does not come from the component; it comes from where the announcement falls on the maintenance calendar. On the collector, whose calendar is seven days, even the longest window never exceeds 6.5 days; on the processing server, whose calendar is thirty days, it can reach 26.5.
This means the question “how fast is a patch applied” cannot be answered with a single number. The server class has the sparsest calendar, and there is a reason for it: a restart means a business disruption, which requires change approval. That same reason stretches the window out to thirty days.
The point-days column reorders the announcements by weight, and the ordering does not follow the day count. The patch-agent announcement on day fifty-one carries 3232 point-days; the system-library announcement on day forty-two carries 770, because it falls into very short windows across all four classes. Part of the difference comes from the number of points affected (204 versus 102), part from calendar alignment. Even if two announcements are both flagged as “same severity,” the weight they carry differs by a factor of four, and the difference is visible only once the point-days are computed.
Three Regimes and the Cost of the Emergency Path
The second table compares three regimes: everything by calendar, only the emergency-flagged announcements by the emergency path, and everything by the emergency path. The average window drops from 10.8 days to 8.6, then to 2.3. Point-days fall from 17,700 to 3801 — close to a fifth.
The same rows carry three cost columns. Of the 48 test items (twelve announcements, four items each), none is skipped under the calendar regime; 12 are skipped under the mixed regime, and 36 under the regime where everything goes by the emergency path. The skipped items are the compatibility test, the rollback rehearsal, and the user notification; the pilot group is preserved under every regime.
The outage columns carry the actual cost. Under the calendar regime, restarts coincide with the maintenance window, so unplanned outages are zero. Under the mixed regime, 184 machines see an unplanned full restart and 82 a service outage. When everything is applied by the emergency path, those numbers become 460 and 479: 939 unplanned outages in 120 days, more than nine per machine in a fleet of one hundred two. This is what an 8.5-day window gain costs, and whoever makes the decision needs to read both numbers side by side.
One number in the third block caps this cost: the emergency path applies in 1.5 days, but the average window stays at 2.3. The 0.8-day gap comes from nine field laptops and one branch endpoint that miss their turn because they stay offline. These ten machines also miss the emergency path and wait for the next calendar day. The floor on shortening the window is not the patch’s readiness time — it is the fleet’s reachability.
The Point That Never Closes
The third block ties the lesson’s distinction to a number. The foothold point count across the fleet is 1038 before the patch, 1038 after. The number of points the patch closes is zero. What closes is 14,080.5 point-days: the total duration for which the points were reachable by a known path.
The 12 in the last column stays the same across all three regimes. The business application on the collector machines cannot be upgraded because of a version-compatibility issue; for 54 days after the announcement, 12 points stay open, and no change of calendar closes them. For this item, the concept of a window loses its meaning, because there is no day on which the window closes.
The remedy for an unpatchable item is not in the patch — it is in the previous lesson’s column: if a point cannot be closed, it should at least be made visible. The definition of the 12 points is written to the baseline and compared against for change. This does not remove the flaw; it makes the moment the flaw is used recordable. That is exactly the difference between the two columns, and for this item there is nothing but the second column.
Summary
- A patch does not remove a foothold point: the point count across the fleet is 1038 both before and after the patch. What a patch reduces is the duration for which the point is reachable by a known path; the number of points it closes is zero.
- The window is a product of the calendar, not the component: the same announcement stays open 2 days on a seven-day calendar and 18 on a thirty-day calendar. A single “average patch time” hides this difference.
- Point-days reorder the announcements: an announcement affecting 204 points carries 3232, another affecting 102 points carries 770. Same severity does not mean same weight.
- The emergency path cuts the average window from 10.8 days to 2.3; in exchange, 36 of 48 test items are skipped and 939 unplanned outages are produced over 120 days.
- The floor on the window is set by the fleet’s reachability, not the patch’s readiness time: the emergency path applies in 1.5 days, the average stays at 2.3, and the gap comes from ten offline machines.
- The unpatchable item leaves 12 points open under all three regimes and stays open for 54 days; its only remedy is the second column — not closing, but recording to the baseline and comparing.
Next Step
Two jobs have been done so far: unnecessary items were pruned, and the known flaws in what remained were closed. The list both jobs rest on was written by hand across these two lessons — someone decided which item was necessary and which calendar was appropriate, and the reasoning behind those decisions was never written down. That gap is the next lesson’s subject: security benchmark guides collect the same decisions item by item and propose a baseline. The measure is not how many items the guide contains, but what those items actually do in this environment: how many items are noncompliant with the endpoints’ baseline, how many are never applicable in this environment, and how many block legitimate work when applied. When the three are not counted separately, only a single compliance percentage remains, and that percentage says nothing about how many points actually close.
To keep your progress and take notes, Log in
My notes
Log in to take notes.