---
title: 'Dependency-Related Risks'
source: 'https://academia.sh/en/courses/secure-coding/dependency-related-risks'
course: 'Secure Coding'
language: en
updated: '2026-09-26T03:02:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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"
---

# 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.

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.

```js
// 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.
