Skip to content
academia.sh

Lesson 09 / 09

Security Debt

Of a 34-item debt ledger, 21 are open and 13 are accepted risk; only 5 of the acceptances carry a validity period and only 2 carry a reassessment point. Of six questions, one is answered fully, four partially, and one is unanswered; making the partial answers full requires writing 26 fields. A four-period queue with 48 person-days of capacity closes 11 items of the 209 person-day ledger, and the mean age of the remaining queue rises from 91 days to 360 days. The 8 accepted items whose validity was never written are never asked about again in any period; of the 3 expired acceptances, only one carries a reassessment point.

Published Updated

In this lesson

  • Separates security debt from technical debt by the validity of an accepted risk and models the debt ledger as two classes
  • Runs the debt as a fixed-capacity queue for four periods: closed items, remaining person-days, and the aging of the queue
  • Counts how an acceptance with no validity and no reassessment point permanently drops out of the ledger
Contents

The previous lesson computed two metrics and showed that both could be improved without touching the thing being measured. The cheapest improvement was converting the old items to accepted risk: the open finding age fell from 59.8 days to 23.4 days, and the number of items actually fixed stayed at zero. Those seven items went somewhere, and where they went is the subject of this lesson.

Accepted risk and open debt together make up security debt. The Software Architecture curriculum measured technical debt; that measurement is not repeated here, and there is a single field that separates security debt from it: an acceptance has a validity. Technical debt does not pay itself off, but it does not expire either; an accepted risk whose validity was never written stops being debt and becomes a record.

The Debt Ledger

AC21. The debt ledger has 34 items: the 21 open items that remained unclosed in the previous lesson and 13 items that were converted to acceptance earlier. The set is a model of the subscriber portal team of the fictional regional measurement network; the generator is written in this block and the seed is visible. AC22. Each item has an effort (person-days) and a risk score; both are fictional data. AC23. An accepted item can carry three fields: a named owner, a validity period, and a reassessment point. AC24. The distribution of the fields is given as data: the first five acceptances have a validity written, the first two a reassessment point, and the first six an owner.

// debt/ledger.mjs — the security debt ledger is a MODEL: fictional regional measurement network, planning time.
// The generator is written here and the seed is visible; the ledger comes out the same on every run.
export const gen = (B) => (...h) => h.map((v, j) =>
  (j ? String(v).padStart(B[j]) : String(v).padEnd(B[0]))).join("");

export const SEED = 41100, N_OPEN = 21, N_ACCEPTED = 13;
let s = SEED;
const rnd = () => (s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 4294967296;

// Of the accepted items, the first five have a validity period, the first two a reassessment
// point, and the first six a named owner; the rest have the field empty. This distribution is data.
const VALIDITY = [90, 120, 180, 180, 365];

export const LEDGER = [];
for (let i = 0; i < N_OPEN + N_ACCEPTED; i++) {
  const isAccepted = i >= N_OPEN, k = i - N_OPEN;
  LEDGER.push({
    code: `D${String(i + 1).padStart(2, "0")}`,
    kind: isAccepted ? "accepted" : "open",
    source: rnd() < (isAccepted ? .75 : .70) ? "non-pipeline" : "pipeline",
    risk: 4 + Math.floor(rnd() * 21),                  // fictional risk score between 4 and 24
    effort: 1 + Math.floor(rnd() * 13),                // person-days
    age: isAccepted ? 40 + Math.floor(rnd() * 380) : 20 + Math.floor(rnd() * 160),
    validity: isAccepted && k < VALIDITY.length ? VALIDITY[k] : null,
    reassess: isAccepted && k < 2,
    owner: isAccepted ? k < 6 : true,
  });
}
export const openItems = LEDGER.filter((d) => d.kind === "open");
export const accepted = LEDGER.filter((d) => d.kind === "accepted");
export const expired = (d) => d.validity !== null && d.age > d.validity;

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`model: ${LEDGER.length} debt items, ${openItems.length} open + ${accepted.length} accepted risk`);
  const y = gen([16, 8, 12, 12, 14, 14]);
  console.log("\n" + y("class", "items", "person-days", "risk total", "has validity", "expired"));
  for (const [name, k] of [["open debt", openItems], ["accepted risk", accepted]])
    console.log(y(name, k.length, k.reduce((a, d) => a + d.effort, 0), k.reduce((a, d) => a + d.risk, 0),
      k.filter((d) => d.validity !== null).length, k.filter(expired).length));

  // The questions asked of a debt item and the field each one needs.
  const QUESTIONS = [["is the item open or accepted", "class", LEDGER.length, LEDGER.length],
    ["who gave the acceptance", "owner", N_ACCEPTED, accepted.filter((d) => d.owner).length],
    ["how long is the acceptance valid", "validity", N_ACCEPTED, accepted.filter((d) => d.validity !== null).length],
    ["has the acceptance expired", "validity+age", N_ACCEPTED, accepted.filter((d) => d.validity !== null).length],
    ["when is the reassessment", "reassessment point", N_ACCEPTED, accepted.filter((d) => d.reassess).length],
    ["which period will the item close", "queue order", LEDGER.length, 0]];
  const answer = (c, b) => c === 0 ? "none" : c === b ? "full" : "partial";
  const z = gen([34, 20, 8, 10, 8]);
  console.log("\n" + z("question", "field needed", "base", "carrying", "answer"));
  for (const [q, f, b, c] of QUESTIONS) console.log(z(q, f, b, c, answer(c, b)));
  const n = (a) => QUESTIONS.filter(([, , b, c]) => answer(c, b) === a).length;
  const missing = (f) => N_ACCEPTED - accepted.filter(f).length;
  const toWrite = missing((d) => d.owner) + missing((d) => d.validity !== null) + missing((d) => d.reassess);
  console.log(`\nof ${QUESTIONS.length} questions, ${n("full")} fully answered, ${n("partial")} partially answered, ` +
    `${n("none")} unanswered`);
  console.log(`to make the partial answers full, ${toWrite} fields are written; of the ` +
    `${accepted.filter(expired).length} items with expired validity, ` +
    `${accepted.filter((d) => expired(d) && d.reassess).length} carries a reassessment point`);
}
model: 34 debt items, 21 open + 13 accepted risk

class              items person-days  risk total  has validity       expired
open debt             21         141         274             0             0
accepted risk         13          68         185             5             3

question                                  field needed    base  carrying  answer
is the item open or accepted                     class      34        34    full
who gave the acceptance                          owner      13         6 partial
how long is the acceptance valid              validity      13         5 partial
has the acceptance expired                validity+age      13         5 partial
when is the reassessment            reassessment point      13         2 partial
which period will the item close           queue order      34         0    none

of 6 questions, 1 fully answered, 4 partially answered, 1 unanswered
to make the partial answers full, 26 fields are written; of the 3 items with expired validity, 1 carries a reassessment point

The first table puts the two classes of the ledger side by side, and the real information is in the last two columns. Of the 13 accepted items, only five have a validity period written; for the other eight, the acceptance is a decision with no end. Of the five with a validity written, three have already expired at planning time, and because nobody asked, all three still sit in the ledger as accepted. The risk total column gives the scale: the accepted items carry 185 of the ledger’s risk total, and the open debt carries 274. Acceptance does not remove the risk; it only moves it out of the column the metrics look at.

In the question table, one of the six questions is answered fully. Of the remaining five, four concern only the accepted items and one concerns the whole ledger; four are partial and one is not answered at all. Making the partial ones full requires writing 26 fields: seven owners, eight validity periods, and eleven reassessment points. This is the cheapest cost in the course, and it needs no pipeline setup or extra volume; what has to be written is the decision itself. The last line gives its counterpart: of the three expired acceptances, only one carries a point that will bring it up again.

Running the Queue

AC25. The capacity is 12 person-days per period, a period is 90 days, and the planning horizon is four periods; all three are fictional data. The ordering rule is risk per unit of effort, and an item that does not fit the capacity is left for the next period. AC26. Two runs are compared: a ledger where an accepted item never comes back, and a ledger where an expired acceptance, if it has a reassessment point, re-enters the queue.

// debt/queue.mjs — the debt is run for four periods as a fixed-capacity queue.
import { LEDGER, openItems, accepted, expired, gen } from "./ledger.mjs";

const CAPACITY = 12, PERIODS = 4, PERIOD_DAYS = 90;   // person-days, number of periods, period length

// The ordering is risk per unit of effort; an item that does not fit the capacity is left for the next period.
// With comeBack on: an expired acceptance that has a reassessment point enters the queue.
const run = (comeBack) => {
  const queue = openItems.map((d) => ({ ...d })), pending = accepted.map((d) => ({ ...d }));
  const rows = [];
  for (let t = 1; t <= PERIODS; t++) {
    let back = 0;
    if (comeBack) for (const d of pending)
      if (!d.requeued && d.reassess && expired(d)) { d.requeued = true; queue.push(d); back++; }
    queue.sort((a, b) => b.risk / b.effort - a.risk / a.effort);
    let left = CAPACITY;
    for (const d of queue) if (!d.closed && d.effort <= left) { d.closed = true; left -= d.effort; }
    const stillOpen = queue.filter((d) => !d.closed);
    rows.push([t, back, queue.filter((d) => d.closed).length, CAPACITY - left, stillOpen.length,
      stillOpen.reduce((a, d) => a + d.effort, 0),
      Math.round(stillOpen.reduce((a, d) => a + d.age, 0) / stillOpen.length)]);
    for (const d of [...stillOpen, ...pending.filter((x) => !x.requeued)]) d.age += PERIOD_DAYS;
  }
  return { rows, stillOpen: queue.filter((d) => !d.closed), pending,
    back: rows.reduce((a, s) => a + s[1], 0) };
};

const y = gen([8, 10, 10, 12, 10, 14, 10]);
const RUNS = [["A: acceptance never returns", false], ["B: expired acceptance returns", true]];
RUNS.forEach(([name, comeBack], i) => {
  const r = run(comeBack);
  console.log((i ? "\n" : "") + name);
  console.log(y("period", "returned", "closed", "spent", "left", "left person-days", "mean age"));
  for (const s of r.rows) console.log(y(...s));
  const hidden = r.pending.filter((d) => !d.requeued);
  console.log(`debt visible in the ledger ${r.stillOpen.length} items / ` +
    `${r.stillOpen.reduce((a, d) => a + d.effort, 0)} person-days; ` +
    `accepted items that never entered the queue ${hidden.length} items / ` +
    `${hidden.reduce((a, d) => a + d.effort, 0)} person-days, risk total ` +
    `${hidden.reduce((a, d) => a + d.risk, 0)}`);
});

console.log(`\ntotal capacity in four periods ${CAPACITY * PERIODS} person-days; the whole ledger ` +
  `${LEDGER.reduce((a, d) => a + d.effort, 0)} person-days`);
console.log(`at planning time ${accepted.filter((d) => expired(d) && d.reassess).length} of the ` +
  `${accepted.filter(expired).length} expired acceptances carries a reassessment point; ` +
  `acceptances that returned to the queue over four periods ${run(true).back}`);
console.log(`${accepted.filter((d) => d.validity === null).length} items with no validity ever written ` +
  `are never asked about again in any period`);
A: acceptance never returns
period    returned    closed       spent      leftleft person-days  mean age
1                0         4          12        17           129        91
2                0         7          12        14           117       175
3                0         9          10        12           107       259
4                0        11          12        10            95       360
debt visible in the ledger 10 items / 95 person-days; accepted items that never entered the queue 13 items / 68 person-days, risk total 185

B: expired acceptance returns
period    returned    closed       spent      leftleft person-days  mean age
1                1         4          11        18           133        87
2                1         7          11        16           130       175
3                0         9          12        14           118       260
4                0        11          11        12           107       349
debt visible in the ledger 12 items / 107 person-days; accepted items that never entered the queue 11 items / 57 person-days, risk total 150

total capacity in four periods 48 person-days; the whole ledger 209 person-days
at planning time 1 of the 3 expired acceptances carries a reassessment point; acceptances that returned to the queue over four periods 2
8 items with no validity ever written are never asked about again in any period

The queue closes 11 items in four periods, and it closes the same number in both runs. The capacity is fixed: 48 person-days over four periods, against a ledger of 209 person-days. This ratio writes the limit of the plan: with this capacity the ledger does not close, it is only ordered. In the third period the capacity spent stays at 10 person-days, because the next item does not fit the two remaining days; a fixed-capacity queue cannot use its capacity in full, and this loss is a consequence of the item size, not of the ordering rule.

The real information is in the last two columns. While the number of remaining items falls from 17 to 10, the mean age rises from 91 days to 360 days. The queue gets shorter and older: the rule of risk per unit of effort pulls the cheap and risky items forward, and the expensive items are carried over to the next period every time. The success of a debt plan is read not from the number of closed items but from the age of the items that never reach the front of the queue.

The difference between the two runs is a single line, and it is the answer of the lesson. In run A, after four periods the ledger shows 10 items and 95 person-days; the 13 accepted items that never entered the queue carry 68 person-days and 185 risk points, and are never asked about in any period. In run B, the two expired acceptances come back, the ledger grows to 12 items, and the hidden load falls from 185 to 150. The growth of the ledger is not a deterioration, it is the widening of the measurement. The reason the number of returning items stays at two is also clear: in eight acceptances the validity was never written, and an acceptance with no validity is the one kind of item that no rule can bring back.

Summary

  • Security debt consists of two classes and is separated from technical debt by a single field: an acceptance has a validity. Of the ledger’s 34 items, 21 are open debt and 13 are accepted risk.
  • Only 5 of the acceptances have a validity written, 2 a reassessment point, and 6 a named owner; of the five with a validity written, three had already expired at planning time.
  • Of six questions, one is answered fully, four partially, and one is unanswered; the cost of making the partial ones full is 26 fields, and this cost needs no pipeline setup or extra volume.
  • Over four periods, 48 person-days of capacity close 11 items of the 209 person-day ledger; while the remaining queue falls from 17 items to 10, its mean age rises from 91 days to 360 days.
  • An acceptance with no validity and no reassessment point permanently drops out of the ledger: 11 of the 13 acceptances are never asked about in any period, and 150 risk points stay outside the measurement.

Course Wrap-Up

Over nine lessons a single question was asked: of the questions to be asked after an incident, how many can be answered, and what does it cost to make them answerable. Each lesson had its own set of items, its own set of questions, and its own cost column.

Lesson How many questions Answerable Cost of making them answerable
Audit Log Design 16 questions 6 → 13 1.45 times the volume, 65→45 days retention, 3 personal data items
Application-Specific Detection Scenarios 18 abuse cases 15 → 18 173 false positives, 101 off-hours pages in 30 days
Application Breach Response 12 steps 5 → 12 from records 630→151 minutes; notifications to 41,200 subscribers without records
Trace in Short-Lived Workloads 5 questions × 2,000 workloads 6,604 / 10,000 pairs 91,084 extra flushes, 33 core-minutes
External Report Intake 91 unique reports unanswered 36 → 0, definite 15 → 20 5 fields, 1.45 times the volume, 20 days shorter retention
Engineering Equivalent of Controls 26 controls 11 queries, 6 documents a practice for 9 controls, a machine trace for 6 documents
Evidence Automation 24 evidence items 12 for the whole period, 6 for the last 90 days 170 hours of setup, 3,017 times the volume, 0.63% sampling rate
Application Security Metrics 8 questions 5 full, 1 partial 568 additional state transition records
Security Debt 6 questions 1 full, 4 partial 26 fields; 48 person-days of capacity for 209 person-days of debt

This course is the fifth and last course of the M25 Application Security and DevSecOps curriculum. Each of the five courses caught the same item at a different stage, and each counted in its own unit.

Course Measure axis Unit
K01 The Secure Development Lifecycle catching stage and multiplier stage
K02 Secure Coding coverage point left outside
K03 Security Automation and the Supply Chain what the audit found, missed, and took from the pipeline findings and minutes
K04 Runtime Security and Secrets Management holding at the most expensive stage missed and wrongly cut
K05 Detection, Response and Compliance answerability of the question unanswered question

The one question asked across the five courses was this: which stage was an item caught at, and what did it cost at that stage. Security is not a stage, it is a decision with a different price at every stage; and the most expensive stage is always the last one. The ledger of this course is the final form of that: an accepted risk is the recorded form of an item that was never caught at any stage, and when its validity is not written, the chance of its being caught drops out of the record as well.

The rule of the curriculum is written once more: an audit whose cost is not written is counted as unmeasured. If the volume, the minutes, the personal data items, and the person-days are not written next to a record, an alert, an evidence item, or a metric, then the only thing known about that audit is that it was done — not what it cost and not what it missed.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close