Skip to content
academia.sh

Lesson 03 / 09

Application Breach Response

In an unauthorized-access breach, twelve response steps are run against four log schemas. In the base schema seven steps are left without a record, the total takes 630 minutes and two steps cannot be completed at all; in the widest schema the same response takes 151 minutes, 4.17 times shorter. The absence of a record prolongs steps by between 4 and 8.3 times. The containment decision is made at minute 10 in every schema — the difference is not in containment but in scoping: when the affected subscriber set cannot be derived, the notification widens from 380 subscribers to an upper bound of 41,200 subscribers.

Published Updated

In this lesson

  • Runs an application breach scenario to derive which log fields each response step depends on
  • Measures how many minutes, and how many times longer, each step takes when its record is absent
  • Counts the cost of a step that cannot be completed, beyond the response itself: a reported upper bound instead of a definite set
Contents

At the end of the previous lesson there is an alert in hand, and the alert is right: a session has read subscriber records it was not authorized to read. Once the alert is verified, the work turns from counting to doing, and the questions change — which account, from where, through which endpoints, how many subscriber records, did data change, is there another account with the same pattern. The phases of the incident response life cycle and the on-call arrangement were measured in the Security Operations and Incident Response course; they are not repeated here.

This lesson measures a single thing: which step needs which record, and how many minutes the step is prolonged if that record does not exist. The breach scenario enters as a log; no exploitation method or payload is described, because what is measured is not how the attack was carried out but how it is understood afterward.

  • DR12. The incident is fictional: unauthorized reads through one account in the subscriber portal; the tenant holds 41,200 subscriber records, and the set actually touched is 380 records.
  • DR13. The incident response playbook is the steps modeled as a data structure. Each step has three attributes: the required log fields, the minutes it takes with the record, and the minutes it takes without the record.
  • DR14. The required fields of a step must be present in a single joinable set; the join rule from the previous lesson applies here as well.
  • DR15. A step whose no-record time is undefined cannot be completed at all: four times the time is spent on a best-effort basis, and an upper bound is reported instead of a definite result.
  • DR16. The times are data; they are not a measurement made on this page. The four schemas are the schemas of the earlier lessons: the base, the one with a join key added, the one with the authorization decision and version added, and the one with the session and the pre-change value added as well.

Step–Record Dependency

// response.mjs — response steps are run in the application breach scenario; each step
// depends on a set of records. IT IS A MODEL: both the incident and the times are fictional.
export const TENANT_SUBSCRIBERS = 41200;   // upper bound to notify if the definite set cannot be derived

// Schema = the log designs of the previous lesson. Fields are kept in joinable sets:
// fields within the same set can be joined, fields in separate sets cannot.
export const SCHEMAS = [
  { name: "base", sets: [["time", "trace", "actor", "action", "outcome", "path", "method", "status",
    "client_ip"], ["time", "table", "object", "operation"]] },
  { name: "+trace", sets: [["time", "trace", "actor", "action", "outcome", "path", "method", "status",
    "client_ip", "table", "object", "operation"]] },
  { name: "+decision+version", sets: [["time", "trace", "actor", "action", "outcome", "path", "method",
    "status", "client_ip", "table", "object", "operation", "authz_decision", "version"]] },
  { name: "+session+value", sets: [["time", "trace", "actor", "action", "outcome", "path", "method",
    "status", "client_ip", "table", "object", "operation", "authz_decision", "version", "session",
    "old_value"]] },
];

// Step = [name, required fields, minutes with record, minutes without record]. If the minutes
// without record are null, the step cannot be completed; four times the time goes in on a best-effort
// basis, and the result is an upper bound.
export const STEPS = [
  ["verify the alert", ["actor", "action", "outcome"], 6, 40],
  ["cut off account and session", ["actor"], 4, 25],
  ["find the entry path", ["client_ip", "trace"], 12, 70],
  ["list the endpoints called", ["path", "method", "trace"], 10, 60],
  ["why authorization was granted", ["actor", "action", "authz_decision"], 14, 100],
  ["affected subscriber set", ["actor", "action", "object"], 25, null],
  ["did data change", ["trace", "table", "operation"], 15, 120],
  ["roll back changed values", ["table", "old_value"], 20, null],
  ["other accounts, same pattern", ["client_ip", "actor"], 18, 90],
  ["version and patch status", ["actor", "version"], 8, 45],
  ["other work in the same session", ["session", "action"], 9, 75],
  ["write the notification scope", ["actor", "object", "time"], 10, 60],
];
export const SCOPE_STEPS = 2;   // the first two steps feed the containment decision

export const hasFields = (schema, fields) =>
  schema.sets.some((c) => fields.every((f) => c.includes(f)));

export function runResponse(schema) {
  let total = 0, containment = 0, missing = 0, uncompletable = 0, definiteSet = true;
  const rows = STEPS.map(([name, fields, withRec, without], i) => {
    const present = hasFields(schema, fields);
    const min = present ? withRec : (without ?? withRec * 4);
    if (!present) { missing++; if (without === null) { uncompletable++;
      if (name === "affected subscriber set") definiteSet = false; } }
    total += min;
    if (i < SCOPE_STEPS) containment += min;
    return { name, min, status: present ? "from log" : (without === null ? "uncompletable" : "manual") };
  });
  return { rows, total, containment, missing, uncompletable,
    notification: definiteSet ? 380 : TENANT_SUBSCRIBERS };
}

export const gen = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j])
  : String(v).padEnd(B[0]))).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const s = SCHEMAS[0], o = runResponse(s);
  console.log(`schema "${s.name}": ${STEPS.length} response steps, ` +
    `${s.sets.length} joinable field sets`);
  const y = gen([32, 34, 10, 15]);
  console.log("\n" + y(["step", "required fields", "minutes", "source"]));
  STEPS.forEach(([name, fields], i) =>
    console.log(y([name, fields.join(","), o.rows[i].min, o.rows[i].status])));
  console.log(`\ntotal ${o.total} minutes, steps without record ${o.missing}/${STEPS.length}, ` +
    `uncompletable ${o.uncompletable}, containment decision at minute ${o.containment}`);
  console.log(`affected subscriber set could not be derived: notification scope ${o.notification} subscribers`);
}
schema "base": 12 response steps, 2 joinable field sets

step                                               required fields   minutes         source
verify the alert                              actor,action,outcome         6       from log
cut off account and session                                  actor         4       from log
find the entry path                                client_ip,trace        12       from log
list the endpoints called                        path,method,trace        10       from log
why authorization was granted          actor,action,authz_decision       100         manual
affected subscriber set                        actor,action,object       100  uncompletable
did data change                              trace,table,operation       120         manual
roll back changed values                           table,old_value        80  uncompletable
other accounts, same pattern                       client_ip,actor        18       from log
version and patch status                             actor,version        45         manual
other work in the same session                      session,action        75         manual
write the notification scope                     actor,object,time        60         manual

total 630 minutes, steps without record 7/12, uncompletable 2, containment decision at minute 10
affected subscriber set could not be derived: notification scope 41200 subscribers

The first four rows of the table are fast, and all of them are read from a single joinable set: who, from where, through which endpoints. These four steps take 32 minutes and are the most visible part of the response. Of the remaining eight rows, seven are without a record, and they produce 598 minutes of the total time. What determines the duration of a response is not the speed of the first steps but which record the middle steps cannot find.

The two most expensive rows are expensive for the same reason. The “did data change” step requires the fields trace, table and operation together; all three fields are written, but because the database change log has no join key, it cannot be shown which request changed which row. The same happens in the “affected subscriber set” step: actor is in one set, object in the other. Neither is a missing field; both are prolonged by a join that was never built. The cost of a missing join is one line in the measurement ledger and two hundred and twenty minutes in the response ledger.

The table also has a row that is easy to overlook, and that row prevents a misreading: the “other accounts, same pattern” step is read from the log even in the base schema, because client_ip and actor are in the same set. This step asks the response’s broadest question — is the incident limited to a single account — and it is answered in eighteen minutes. The value of a log design is not distributed by the number of steps but by which steps can be read from the log; the cheapest field can be the one that answers the broadest question.

The last two rows write down the cost of the log design that spills beyond the response. When the affected subscriber set cannot be derived definitely, an upper bound is reported in its place: all 41,200 subscribers in the tenant. The set actually touched is 380 records. The gap between them is not a reporting detail; it determines the notification obligation, the load on the call center and the sentence the organization says to the outside world. Similarly, the “roll back changed values” step cannot be completed at all because the pre-change value was not written — the data cannot be rolled back, it can only be said which rows changed. Neither step gives the spent time back: the model writes 100 and 80 minutes for these steps respectively, and at the end of that time what is left in hand is not a definite set but a range. An uncompletable step is not a skipped step; its cost is paid twice, once in minutes and once in the certainty of the answer.

The Same Response, Four Log Schemas

// compare.mjs — the same response is run against four log schemas; the difference in log design
// is translated into minutes and notification scope. IT IS A MODEL.
import { SCHEMAS, STEPS, runResponse, hasFields, gen } from "./response.mjs";

const y = gen([19, 11, 17, 12, 15, 14]);
console.log(y(["schema", "total min", "containment min", "no record", "uncompletable", "notification"]));
const results = SCHEMAS.map((s) => {
  const o = runResponse(s);
  console.log(y([s.name, o.total, o.containment, `${o.missing}/${STEPS.length}`, o.uncompletable,
    o.notification]));
  return o;
});

// Prolongation per step: how many minutes the same step takes in the base schema and in the widest schema.
const base = SCHEMAS[0];
const prolonged = STEPS.map(([name, fields, withRec, without]) => ({ name, withRec,
  without: hasFields(base, fields) ? withRec : (without ?? withRec * 4),
  definite: without !== null || hasFields(base, fields) }))
  .filter((u) => u.without !== u.withRec).sort((a, b) => (b.without - b.withRec) - (a.without - a.withRec));
const z = gen([32, 13, 16, 8, 13]);
console.log("\nsteps prolonged in the base schema (compared with the widest schema)");
console.log(z(["step", "with record", "without record", "factor", "result"]));
for (const u of prolonged)
  console.log(z([u.name, u.withRec, u.without, (u.without / u.withRec).toFixed(1),
    u.definite ? "definite" : "upper bound"]));

const t = results[0], g = results.at(-1);
console.log(`\nbase schema ${t.total} minutes, widest schema ${g.total} minutes: ` +
  `${t.total - g.total} minutes difference, ${(t.total / g.total).toFixed(2)} times`);
console.log(`the containment decision can be made at minute ${g.containment} in every schema; ` +
  `the difference is not in containment but in scoping`);
console.log(`notification scope narrows from an upper bound of ${t.notification} subscribers to ` +
  `a definite set of ${g.notification} subscribers: ${(t.notification / g.notification).toFixed(0)} times narrower`);
console.log(`the second schema (+trace) saves ${t.total - results[1].total} minutes with a single field ` +
  `and lowers the uncompletable steps from ${t.uncompletable} to ${results[1].uncompletable}`);
schema               total min  containment min   no record  uncompletable  notification
base                       630               10        7/12              2         41200
+trace                     400               10        4/12              1           380
+decision+version          277               10        2/12              1           380
+session+value             151               10        0/12              0           380

steps prolonged in the base schema (compared with the widest schema)
step                              with record  without record  factor       result
did data change                            15             120     8.0     definite
why authorization was granted              14             100     7.1     definite
affected subscriber set                    25             100     4.0  upper bound
other work in the same session              9              75     8.3     definite
roll back changed values                   20              80     4.0  upper bound
write the notification scope               10              60     6.0     definite
version and patch status                    8              45     5.6     definite

base schema 630 minutes, widest schema 151 minutes: 479 minutes difference, 4.17 times
the containment decision can be made at minute 10 in every schema; the difference is not in containment but in scoping
notification scope narrows from an upper bound of 41200 subscribers to a definite set of 380 subscribers: 108 times narrower
the second schema (+trace) saves 230 minutes with a single field and lowers the uncompletable steps from 2 to 1

The first column is the log design translated into response time: the same incident, the same steps, the same people; a 4.17 times difference between 630 minutes and 151 minutes. The second row shows where this difference comes from. A single join key — the 18-byte trace added to the database record — shortens the response by 230 minutes and lowers the uncompletable steps from two to one. In the previous lesson the same field had raised the number of answered questions from six to eleven; here the second invoice for the same field is paid.

The second column cuts off a misreading. The containment decision — suspending the account and cutting off the session — can be made at minute 10 in all four schemas, because that step requires only the actor field, and that field is present in every schema. Log design does not change how fast the attack is stopped; what it changes is how fast you can say what happened. The reason breach responses take long is more often not containment but scoping: whose data was touched, what changed, and whether another account shows the same pattern.

The order of the schemas carries information too. The move from the first schema to the second saves 230 minutes for one field; from the second to the third, 123 minutes for two fields; from the third to the fourth, 126 minutes for two fields. The gain per field is 230, 62 and 63 minutes. The join key saves more than three times as much as the others, and it does so without recording any new fact. On the volume side the same order reverses: the join key grows the daily volume by 2%, the last two fields by 39%. Log fields are not priced equally; there are multiples of difference both between their gains and between their costs, and the two rankings are the reverse of each other.

The prolongation table writes the cost step by step. The factor column ranges between 4.0 and 8.3, and the highest factors fall on the smallest steps: the nine-minute “other work in the same session” step takes 75 minutes without a record. The result column brings a sharper distinction. Five steps give a definite result even without a record — they only take longer. When two steps are left without a record, they cannot give a definite result; what the 100 and 80 spent minutes buy is an upper bound. A step’s record dependency is of two kinds: one determines the time, the other the certainty of the answer; the second cannot be closed with any amount of human hours.

The limit of the model must also be written down: the total minutes are a one-person calculation. Most steps do not wait for one another, and when three people work, the elapsed time is smaller than the total minutes. But splitting divides only the time; it does not divide the certainty: when all three run the “affected subscriber set” step without a record, the result is still an upper bound. In the same way, the prolongation of the steps without a record can be closed with headcount, while the two uncompletable steps cannot be closed with any staffing. What can be shortened by adding people and what cannot are separate columns; when a response plan confuses the two, it hands a job that cannot be solved by increasing staff over to staffing.

The other face of the cost was counted in the previous lesson and must be read again here. The widest schema makes a 151-minute response possible, but raises the daily volume to 1.45 times, lowers retention from 65 days to 45, and raises the personal data items to three. With 45-day retention, an incident at the start of the 60-day audit period never gets to use those 151 minutes — the record is not there. A field taken for response speed buys nothing once the incident’s window has slipped away.

Summary

  • A response playbook is measured not as a list of steps but as a step–record dependency: the required fields, the time with the record and the time without the record of each step are written separately.
  • In the base schema 7 of the 12 steps are left without a record and the response takes 630 minutes; in the widest schema the same response takes 151 minutes, a 4.17 times difference.
  • Most of the prolongation comes not from a missing field but from a join that was never built; a single join key saves 230 minutes and lowers the uncompletable steps from two to one.
  • Log design does not change the speed of the containment decision — minute 10 in every schema — only the speed of scoping.
  • Record dependency is of two kinds: five steps give a definite result even without a record but take 5.6 to 8.3 times longer; two steps cannot give a definite result, and the notification widens from 380 subscribers to an upper bound of 41,200 subscribers.
  • The cost of a fast response is written in the previous lesson’s ledger: 1.45 times the volume, retention falling from 65 days to 45, and three personal data items.

Next Step

The whole calculation of this lesson rested on one assumption: if a log field was written, it is in hand at the time of the response. In a persistent service this is true. In short-lived workloads it is not — when a task container lives for forty seconds and is terminated, only the log items it managed to write out can be queried; the rest goes away with the process. The next lesson measures this window: which application items can get out before the workload dies, what happens to the share of collectable items as the level of detail rises, and how many of the earlier lessons’ questions the items left in hand can answer.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close