Lesson 17 / 17
Dependency-Related Risks
The dependency graph's coverage measure: eight names appear in the manifest file while thirty-four packages exist in the process, the transitive depth rising to six, a single component affecting twenty packages, three of eight direct dependencies never being called, and four of six open findings arriving with the same severity being in reachable components and two being in an unreached component.
Published Updated
In this lesson
- Computes the transitive depth and the size of the transitive closure in the dependency graph
- Derives the number of packages a single component transitively affects, using reverse reachability
- Separates the loaded component from the called one: counts why a flaw in an unreached component is prioritized differently
- Rounds up the course's seventeen lessons in a coverage table
Contents
The previous lesson counted the points where the process talks outward, and in every one of them what was measured was the behavior of the code we wrote ourselves: which line puts the stack trace into the response, which field gets masked. Yet most of the code that serves a request is not lines the team wrote. When the portal service’s process comes up, dozens of components get loaded; part of them appear by name in a manifest file, the rest come along only because they are some other component’s dependency.
The Dependency Risk lesson treated this set as a configuration decision: how many days of exposure the upgrade policy accumulates, how many versions it breaks. Here, policy is never discussed. What is measured is the graph’s own structure — how many components there are, how far apart they sit, how many are affected when one falls, and how many stay loaded in the process without ever being called.
The Dependency Graph
SC13 — the number of components named in the manifest file does not give the number of components loaded in the process; the latter is the transitive closure and a separate quantity. SC14 — the area a component affects is measured by the number of packages transitively depending on it, and is independent of depth. SC15 — being loaded is not being called; a component unreached from the code path still sits in the graph, still takes up space in the process, and never runs.
// dependency/graph.mjs — the dependency graph of the fictional measurement-network portal // service (model). Eight components appear by name in the manifest file; the rest come in // transitively. Edges are generated with our own generator, the seed is visible. Package // names are fictional (p01, p02, ...). const SEED = 91237; let state = SEED; const random = () => (state = (state * 1103515245 + 12345) % 2 ** 31) / 2 ** 31; const N = 44, DIRECT = 8; const name = (i) => `p${String(i).padStart(2, '0')}`; const edges = new Map(Array.from({ length: N }, (_, i) => [i, []])); edges.set(-1, Array.from({ length: DIRECT }, (_, i) => i)); // -1 = our own code for (let i = 0; i < N; i++) { const count = i < DIRECT ? 1 + Math.floor(random() * 4) : Math.floor(random() * 3); for (let k = 0; k < count; k++) { const target = DIRECT + Math.floor(random() * (N - DIRECT)); if (target !== i && !edges.get(i).includes(target)) edges.get(i).push(target); } } // Depth: shortest distance from our own code. A package that is never reached is not in the graph. const depth = new Map(); let frontier = [-1]; for (let level = 0; frontier.length; level++) { const next = []; for (const u of frontier) for (const v of edges.get(u) ?? []) { if (!depth.has(v)) { depth.set(v, level + 1); next.push(v); } } frontier = next; } const packages = [...depth.keys()].sort((a, b) => a - b); // The packages a single component affects: the number of packages transitively depending on it. const reverseReach = (target) => { const seen = new Set(); let changed = true; while (changed) { changed = false; for (const u of packages) { if (seen.has(u)) continue; if ((edges.get(u) ?? []).some((v) => v === target || seen.has(v))) { seen.add(u); changed = true; } } } return seen.size; }; // Our code path actually calls five of the eight direct components; three are loaded but never called. const CALLED = [0, 1, 3, 4, 6]; const reached = new Set(); frontier = [...CALLED]; while (frontier.length) { const node = frontier.pop(); if (reached.has(node)) continue; reached.add(node); for (const v of edges.get(node) ?? []) frontier.push(v); } const write = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join('')); const distribution = {}; for (const p of packages) distribution[depth.get(p)] = (distribution[depth.get(p)] ?? 0) + 1; console.log(`seed ${SEED}: ${packages.length} packages in the graph, ${DIRECT} of them appear ` + `by name in the manifest file, ${packages.length - DIRECT} of them are transitive`); console.log(`distribution by depth: ${Object.entries(distribution).map(([k, v]) => `${k}: ${v}`).join(', ')}`); console.log(`largest depth ${Math.max(...depth.values())}, ` + `edge count ${packages.reduce((a, p) => a + (edges.get(p) ?? []).length, 0)}`); console.log(`packages reached from our code path ${reached.size}, loaded but unreached ` + `${packages.length - reached.size}`); console.log(`depth no one chose: ${packages.filter((p) => depth.get(p) >= 3).length} packages at ` + `three and beyond, ${packages.filter((p) => depth.get(p) >= 5).length} packages at five and beyond`); const impact = packages.map((p) => [p, reverseReach(p)]).sort((a, b) => b[1] - a[1]); console.log('\nthe five components touching the most packages:'); const D = [-10, 10, 12, 18]; write(D, 'package', 'depth', 'affected', 'reachable'); for (const [p, n] of impact.slice(0, 5)) { write(D, name(p), depth.get(p), n, reached.has(p) ? 'yes' : 'no'); } // Six fictional open findings: nothing is given except which package each is in. const OPEN_FINDINGS = [17, 29, 12, 4, 27, 7]; // fictional findings; only the package name is given console.log('\nsix open findings, all with the same severity:'); const E = [-10, 10, 12, 18, 8]; write(E, 'package', 'depth', 'affected', 'reachable', 'rank'); const sorted = OPEN_FINDINGS .map((p) => ({ p, e: reached.has(p), n: reverseReach(p) })) .sort((a, b) => (b.e - a.e) || (b.n - a.n)); sorted.forEach((s, i) => write(E, name(s.p), depth.get(s.p), s.n, s.e ? 'yes' : 'no', i + 1)); console.log(`${sorted.filter((s) => s.e).length} in reachable packages, ${sorted.filter((s) => !s.e).length} ` + `in unreached packages; both came in with the same severity`);
seed 91237: 34 packages in the graph, 8 of them appear by name in the manifest file, 26 of them are transitive distribution by depth: 1: 8, 2: 15, 3: 6, 4: 2, 5: 2, 6: 1 largest depth 6, edge count 44 packages reached from our code path 28, loaded but unreached 6 depth no one chose: 11 packages at three and beyond, 3 packages at five and beyond the five components touching the most packages: package depth affected reachable p17 3 20 yes p24 3 20 yes p36 6 19 yes p10 5 18 yes p13 2 18 yes six open findings, all with the same severity: package depth affected reachable rank p17 3 20 yes 1 p29 4 18 yes 2 p12 2 6 yes 3 p04 1 0 yes 4 p27 2 3 no 5 p07 1 0 no 6 4 in reachable packages, 2 in unreached packages; both came in with the same severity
The Depth No One Chose
The first line puts two numbers side by side: eight names appear in the manifest file, thirty-four packages sit in the process. No one chose the twenty-six components in between; none of them was the subject of a decision, none of their names appear in a file the team wrote. For coverage purposes this number is decisive: every audit done on the codebase — review, rule, inspection — covers the eight names and never touches the twenty-six.
The depth distribution details the distance. There are eight packages at the first level, fifteen at the second, six at the third; five more across the fourth, fifth, and sixth levels. Eleven packages sit at depth three and beyond, three at depth five and beyond. Reaching a component at the sixth level means following five separate components’ dependency lists; none of those lists is ours, and each one can change independently.
It also needs to be said that depth is not on its own a measure of risk. Depth only tells who made the decision: the first level is our decision, the second level is our direct dependency’s decision, the sixth level is the composite of five decisions. The column that measures risk is the next one.
The Area a Single Component Touches
The second table looks in the reverse direction: how many packages’ dependency chain a component appears in. The top two components affect twenty of the thirty-four packages; when a flaw is found in one of them, twenty packages become relevant at once. The third row shows that this measure is independent of depth: the component at the sixth level affects nineteen packages, meaning the graph’s most distant component is also one of its most-touching ones. Distance and impact do not track each other, and the two must be measured separately.
The graph’s edge count says where this pile-up comes from: there are forty-four edges among the thirty-four packages, an average of 1.3 outgoing edges per package. The number is small, but the graph is not a tree — the same component is reached by more than one path. In a tree, the number of packages a leaf affects is zero, the number the root affects is the whole of it, and there is a clean drop-off in between. In a shared graph, even a node in the middle can touch twenty packages; the pile-up in the impact column is the direct result of this sharing, and it is born even with a small number of edges.
The direct dependencies at the first level have an affected count of zero; this is not a flaw, it is the definition of the measure. Nothing depends on them, only our own code does. A problem in a direct dependency concerns a single component; a problem in a deep shared component concerns twenty — and both arrive in the same record format, with the same severity. The record itself does not carry this difference; only the graph gives it.
The Flaw in a Component That Is Never Called
The last measurement adds a second filter to the graph. Five of the eight direct dependencies are actually called by our code path, three are loaded but never called. Together with the transitive closure of those three, six packages sit in the process and never run at all; the number of reached packages is twenty-eight.
When the six open findings arrive with the same severity, the table produces a ranking, and two columns decide that ranking: first reachability, then the number of packages affected. The first-ranked finding is both in a reachable component and affects twenty packages. The fourth-ranked finding is also in a reachable component, but its affected count is zero — a first-level dependency. The two findings ranked fifth and sixth are in components that are never called; one affects three packages, the other none.
It needs to be written out plainly why a flaw in an unreached component gets a different priority, because this distinction is often misread. A flaw in an unreached component is not ignored: the component is loaded in the process, a code change tomorrow could call it, and the reachability measurement is an estimate made against our own graph, not a certain result. The distinction is not in the decision to close it but in the order of closing it. All six of the six findings get closed; the first three get closed first, because leaving them open concerns a code path running today.
The number that matters for coverage is this: of the six findings that arrive with the same severity, four are in reachable components, two are in an unreached component. A setup that does not make this distinction treats all six findings as equal and spends a third of its review time on code that never runs. A setup that makes the distinction has to answer two questions at once: which component is called, and when was this measurement last refreshed. The second question is outside this course’s scope.
Summary
- Eight names appear in the manifest file while thirty-four packages sit in the process; twenty-six components were never the subject of a decision and no audit in the codebase touches them.
- The depth distribution gives eight packages at the first level, fifteen at the second; eleven packages sit at depth three and beyond, three at depth five and beyond. Depth measures not risk but who made the decision.
- A single component affects twenty of the thirty-four packages, and the component at the sixth level affects nineteen; distance and impact do not track each other.
- Three of the eight direct dependencies are loaded but never called; together with their transitive closure, six packages sit in the process and never run.
- Of six open findings arriving with the same severity, four are in reachable components and two are in an unreached component; the distinction decides the order of closing, not the decision to close.
Course Wrap-Up
| Lesson | How Many Points | What the Pattern Covers | Uncovered |
|---|---|---|---|
| Input Validation Patterns | 15 field points | allowlist 15 | 0 points (denylist 14) |
| Output Encoding | 14 output points | context declaration 14 | 0 points (scattered 5) |
| Injection Defense | 16 query points | parameterized 11 | 5 edge-case flows |
| Template and Expression Injection | 12 templates, 65 names | restricted environment 40 names | 25 names, 5 templates |
| File Upload Security | 5 upload points | central 5 points | 4 points, 10 layers |
| Server-Side Request Forgery | 12 outbound targets | address restriction 8 | 0 targets (name list 3) |
| Insecure Deserialization | 9 types, 11 nodes | allowlist 5 types | 0 side-effectful (open 4) |
| Authentication Implementation Errors | 8 points, 3 channels | central validator 8 | 0 points (distributed 5) |
| Session Management Errors | 7 flows | 3 flows | 4 flows |
| Broken Access Control | 14 endpoints | 9 endpoints | 5 endpoints |
| Business Logic Flaws | 625 paths | state machine 625 | 0 paths (per step 501) |
| Race Conditions | 7 checkpoints | 3 points | 4 points, 27 excess passes |
| Cryptographic Misuse | 14 call points | 7 points | 7 points, 5 flawed |
| Randomness | 9 token points | 3 points | 6 points, 5 consequential |
| Hardcoded Secrets | 5 secrets, 60 commits | pattern scan 4 | 1 secret, 198 open commits |
| Error Messages and Log Leakage | 8 endpoints, 11 items | 6 endpoints, 9 items | 2 endpoints, 2 items |
| Dependency-Related Risks | 34 packages | 8 direct names | 26 transitive, 6 unreached |
The table asks the same question seventeen times and answers it seventeen times with a different unit: field point, output point, query point, template, upload point, outbound target, flow, endpoint, path, checkpoint, call point, secret, log item, package. The units being different does not mean the measure changes — the third column says the same thing on every row: not the number of points that are vulnerable today, but the number that could become vulnerable tomorrow.
The middle column should not be read on its own either. On some rows, the number the pattern covers equals the first column — the allowlist covers fifteen of fifteen field points, the state machine covers 625 of 625 paths. On other rows it does not, and the gap shows a class of point the pattern does not cover: the parameterized interface covers eleven of sixteen query points, because five edge-case flows never call it at all. In the first case, the defense closed off a path; in the second, it offered an option.
This is also where the course’s rule is read from. Secure coding is not a list of rules, it is how many points a pattern closes on its own. None of the rows with a zero in the table’s third column had a large rule count; in every one of them, the defense had been placed somewhere a point could not step outside of — a type chart, a context declaration, a parameterized interface, a state machine, a single-writer queue, a primitive call sealed inside a single file. On the rows that are not zero, the defense had been written correctly, but it had to be revisited at every new point. A defense that has to be manually repeated at every new point is a defense that will eventually be forgotten; the table’s uncovered column is today’s count of that forgetting.
There are two questions this course does not ask. The first is who counts all this coverage on every change: the seventeen measurements here ran on hand-built inventories, and the inventory itself goes stale as the code changes. The second is that all of the measurements stay inside the codebase. This last lesson’s graph showed the limit of that: twenty-six components come from lines we did not write, and a code review never reaches them. The same is true of a build’s output, a deployed image, and every layer that goes into that image; none of it was counted in this course. M25/K03 Security Automation and the Supply Chain takes up both of these questions together: who counts coverage on every change, and how the layer outside the code gets measured.
To keep your progress and take notes, Log in
My notes
Log in to take notes.