Skip to content
academia.sh

Lesson 13 / 16

Event Logs

In a fictional endpoint fleet, twelve foothold point types are measured against seven log sources: under the default configuration, 932 of 3,078 (machine, type) pairs are visible by name and 1,092 are in no source at all; once every source is opened, the visible-by-name count rises to 2,356, 250 pairs stay blind, and the cost is 10.2 million extra records a day plus 5 configuration items per role profile.

Published Updated

In this lesson

  • Measures endpoint log sources as coverage over foothold point types
  • Separates records that name the item from records that only leave a trace: counts full, indirect, and blind visibility
  • Calculates the configuration-item and daily-volume cost of opening closed sources
Contents

The previous lesson counted persistence mechanisms type by type and recorded what each protection layer misses. The only thing that can show a missed item was missed is a record: if the protection layer could not stop it, all that is left is what it saw. This topic measures those records, and the first question is the plainest one — when a foothold is set up on the endpoint, which source writes it down.

This topic’s unit of measure is visibility, and its two columns must not be conflated. A record either writes the item by name (which scheduled task, which service definition, which login script) or only shows that something happened: a session opened, a process started, a configuration changed. The second kind starts an investigation, it does not answer it. The measurement below is a model and the regional measurement network is fictional; source type is used, not product name.

  • SL1. The endpoint fleet is fictional: 84 field laptops, 46 branch endpoints, 120 meter collector machines, 18 central processing servers. Platform families are referred to by their security model, as in the Information Technology and Systems Fundamentals course: the family with discrete user and group permissions, the family with access control lists, the family relying on a signed app store.
  • SL2. There are twelve foothold point types, and not every type exists in every family; the family with access control lists has no counterpart for three of them.
  • SL3. A source sees a type fully (writes the item by name), indirectly (the event is recorded, the item stays unnamed), or not at all.
  • SL4. Two sources are closed by default: the process creation log and the scheduler log. Turning them on requires a configuration item and brings volume with it. Log volumes are per-machine averages and are fictional.
  • SL5. The application audit log exists only on machines that run the application; the management plane audit log only on centrally managed machines. Both were measured in earlier courses; here they are just sources.

What the Sources See

// source.mjs — coverage between log sources and foothold point types. MODEL.
// The regional measurement network is FICTIONAL; no real organization, machine, or product name is used.

// Foothold point type -> which platform families it exists in.
// a: family with discrete user and group permissions, e: family with access control lists,
// i: family relying on a signed app store.
export const POINT = {
  "startup entry": "aei", "system service definition": "aei", "user service definition": "ai",
  "scheduled task": "aei", "login script": "aei", "desktop autostart entry": "aei",
  "package install hook": "ai", "kernel module": "aei", "bootloader boot entry": "ai",
  "management agent plugin": "aei", "in-app scheduled job": "aei",
  "firmware startup entry": "aei",
};

// Source: open by default?, log records/machine (fictional), configuration items to turn it on,
// type it sees -> "full" (writes the item by name) or "indirect" (event visible, item unnamed).
export const SOURCE = {
  "system event log": { open: true, volume: 4200, item: 0, sees: {
    "system service definition": "full", "kernel module": "full",
    "startup entry": "indirect", "management agent plugin": "indirect" } },
  "authentication log": { open: true, volume: 1900, item: 0, sees: {
    "login script": "indirect", "desktop autostart entry": "indirect" } },
  "process creation log": { open: false, volume: 38000, item: 3, sees: {
    "startup entry": "full", "system service definition": "full",
    "user service definition": "full", "scheduled task": "full", "login script": "full",
    "desktop autostart entry": "full", "package install hook": "full",
    "in-app scheduled job": "full", "bootloader boot entry": "indirect",
    "kernel module": "indirect", "management agent plugin": "indirect" } },
  "package and installation log": { open: true, volume: 40, item: 0, sees: {
    "package install hook": "full", "kernel module": "indirect" } },
  "scheduler log": { open: false, volume: 320, item: 2, sees: {
    "scheduled task": "full" } },
  "application audit log": { open: true, volume: 2400, item: 0, sees: {
    "in-app scheduled job": "full" } },
  "management plane audit log": { open: true, volume: 60, item: 0, sees: {
    "management agent plugin": "full", "firmware startup entry": "full" } },
};

// Machine group: family, machine count, sources present on it (fictional inventory).
export const GROUP = [
  ["field laptop", "i", 84, ["system event log", "authentication log",
    "process creation log", "package and installation log", "scheduler log"]],
  ["branch endpoint", "e", 46, ["system event log", "authentication log",
    "process creation log", "package and installation log", "scheduler log"]],
  ["meter collector", "a", 120, ["system event log", "authentication log",
    "process creation log", "package and installation log", "scheduler log",
    "application audit log"]],
  ["central processing", "a", 18, Object.keys(SOURCE)],
];

// For one type in one group: the best among open sources is "full", "indirect", or "none".
export const visibility = (type, sources, allOpen) => {
  let best = "none";
  for (const s of sources) {
    if (!allOpen && !SOURCE[s].open) continue;
    const g = SOURCE[s].sees[type];
    if (g === "full") return "full";
    if (g === "indirect") best = "indirect";
  }
  return best;
};
export const types = (family) => Object.keys(POINT).filter((t) => POINT[t].includes(family));

const row = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j])
  : String(v).padEnd(B[0]))).join("");
const line = row([33, 12, 8, 11, 9, 16]);

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`model: ${Object.keys(POINT).length} foothold point types, ` +
    `${Object.keys(SOURCE).length} log sources, ${GROUP.length} machine groups (fictional)`);
  console.log(line(["log source", "default", "full", "indirect", "blind",
    "rec./day/mach."]));
  for (const [name, k] of Object.entries(SOURCE)) {
    const d = Object.values(k.sees);
    console.log(line([name, k.open ? "open" : "closed", d.filter((x) => x === "full").length,
      d.filter((x) => x === "indirect").length,
      Object.keys(POINT).length - d.length, k.volume]));
  }
  const byDefault = Object.keys(POINT).filter((t) =>
    visibility(t, Object.keys(SOURCE), false) === "none");
  const withAll = Object.keys(POINT).filter((t) =>
    visibility(t, Object.keys(SOURCE), true) !== "full");
  console.log(`\ntypes no source writes by name even with all sources open: ` +
    `${withAll.length} (${withAll.join(", ")})`);
  console.log(`types left with no trace under the default open sources: ` +
    `${byDefault.length} (${byDefault.join(", ")})`);
}
model: 12 foothold point types, 7 log sources, 4 machine groups (fictional)
log source                            default    full   indirect    blind  rec./day/mach.
system event log                         open       2          2        8            4200
authentication log                       open       0          2       10            1900
process creation log                   closed       8          3        1           38000
package and installation log             open       1          1       10              40
scheduler log                          closed       1          0       11             320
application audit log                    open       1          0       11            2400
management plane audit log               open       2          0       10              60

types no source writes by name even with all sources open: 1 (bootloader boot entry)
types left with no trace under the default open sources: 3 (user service definition, scheduled task, bootloader boot entry)

The first thing that stands out in the table is that none of the three sources open by default on every machine is broad. The system event log writes two of the twelve types by name, the authentication log writes none of them — that source’s job is not tracking items, it is tracking sessions, and it sees only two types indirectly. The package and installation log writes one type. The default configuration’s coverage is these three rows.

The only source that writes eight types by name is the one that is closed. The process creation log is broad because what foothold points have in common is that they eventually run something; the process that installs the item and the job the item runs both create a process. The same row carries the cost too: 38,000 records per machine per day, more than four times the total of the other six sources combined.

The last two rows rescue three types. The in-app scheduled job appears only in the application’s own audit log; the management agent plugin and the firmware startup entry appear only in the management plane audit log. These are types that cannot be seen from inside the endpoint: firmware runs before the operating system, and the agent plugin is pushed from a central point and leaves no installation record on the machine.

The last row shows the measurement’s limit. Even with every source open, one type is never written by name in any source: the bootloader boot entry is visible only indirectly — the process creation log has processes that were started under it, but the entry itself is never named anywhere.

Coverage by Machine Group

// coverage.mjs — coverage per machine group: default configuration vs. everything open.
import { POINT, SOURCE, GROUP, visibility, types } from "./source.mjs";

const count = (group, allOpen) => {
  const [, family, , sources] = group;
  const d = { full: 0, indirect: 0, none: 0, blindSpots: [] };
  for (const t of types(family)) {
    const g = visibility(t, sources, allOpen);
    d[g === "none" ? "none" : g]++;
    if (g === "none") d.blindSpots.push(t);
  }
  return d;
};
const extra = (group) => group[3].filter((s) => !SOURCE[s].open)
  .reduce((a, s) => ({ item: a.item + SOURCE[s].item, volume: a.volume + SOURCE[s].volume }),
    { item: 0, volume: 0 });

const row = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j])
  : String(v).padEnd(B[0]))).join("");
const line = row([20, 7, 10, 7, 7, 10, 7, 7, 11, 8, 13]);

console.log(line(["machine group", "family", "machines", "types", "full", "indirect", "none",
  "full+", "indirect+", "none+", "extra items"]));
let pairs = 0, fullDefault = 0, fullAll = 0, noneDefault = 0, noneAll = 0, dailyExtra = 0;
for (const g of GROUP) {
  const v = count(g, false), h = count(g, true), e = extra(g), n = g[2];
  pairs += n * types(g[1]).length;
  fullDefault += n * v.full; fullAll += n * h.full; noneDefault += n * v.none;
  noneAll += n * h.none;
  dailyExtra += n * e.volume;
  console.log(line([g[0], g[1], n, types(g[1]).length, v.full, v.indirect, v.none,
    h.full, h.indirect, h.none, e.item]));
}
const machines = GROUP.reduce((a, g) => a + g[2], 0);
console.log(`\nfleet: ${machines} machines, ${pairs} (machine, type) pairs`);
console.log(`default configuration: ${fullDefault} pairs visible by name, ${noneDefault} ` +
  `pairs in no source`);
console.log(`all sources open: ${fullAll} pairs visible by name, ${noneAll} pairs in no source`);
for (const g of GROUP) {
  const h = count(g, true);
  console.log(`${g[0].padEnd(20)} blind spots: ${h.blindSpots.join(", ") || "-"}`);
}
const dailyRecords = GROUP.reduce((a, g) => a + g[2] * g[3]
  .filter((s) => SOURCE[s].open).reduce((sum, s) => sum + SOURCE[s].volume, 0), 0);
console.log(`\ndaily records: default ${dailyRecords.toLocaleString("en-US")}, ` +
  `all open ${(dailyRecords + dailyExtra).toLocaleString("en-US")} ` +
  `(${((dailyRecords + dailyExtra) / dailyRecords).toFixed(1)}x)`);
console.log(`gain: +${fullAll - fullDefault} pairs become visible by name; cost is ` +
  `${GROUP.reduce((a, g) => a + extra(g).item, 0)} configuration items ` +
  `(${extra(GROUP[0]).item} per role profile) and ` +
  `${dailyExtra.toLocaleString("en-US")} extra records per day`);
console.log(`even with all sources open, ${noneAll} pairs stay blind; all are the same type ` +
  `and correspond to one of ${Object.keys(POINT).length} types`);
machine group        family  machines  types   full  indirect   none  full+  indirect+   none+  extra items
field laptop              i        84     12      3         4      5      9          2       1            5
branch endpoint           e        46      9      2         4      3      7          1       1            5
meter collector           a       120     12      4         4      4      9          2       1            5
central processing        a        18     12      6         3      3     11          1       0            5

fleet: 268 machines, 3078 (machine, type) pairs
default configuration: 932 pairs visible by name, 1092 pairs in no source
all sources open: 2356 pairs visible by name, 250 pairs in no source
field laptop         blind spots: firmware startup entry
branch endpoint      blind spots: firmware startup entry
meter collector      blind spots: firmware startup entry
central processing   blind spots: -

daily records: default 1,977,800, all open 12,247,560 (6.2x)
gain: +1424 pairs become visible by name; cost is 20 configuration items (5 per role profile) and 10,269,760 extra records per day
even with all sources open, 250 pairs stay blind; all are the same type and correspond to one of 12 types

At fleet scale the question is: how many of the 3,078 (machine, type) pairs are visible by name. Under the default configuration the answer is 932, less than a third; 1,092 pairs are not visible in any source at all. This second number is the lesson’s main finding, because a coverage gap does not produce an alert: a closed source is silent, and the inventory’s “logs are being collected” line is still true.

The group rows show where the difference comes from. Central processing servers see six of the twelve types by name under the default configuration, field laptops see three. The difference is the presence of two sources; the application audit log and the management plane audit log are sources that have no counterpart on a laptop. The weakest visibility at the endpoint sits on the organization’s most scattered and most physically unprotected machines.

The branch endpoint row calls for reading a distinction. In that group the type count is nine, not twelve, and the number of invisible pairs is lower for that reason — three types do not exist in that family; they are not closed, they never existed. If a coverage table’s percentage calculation does not separate these two situations, the narrowest platform reads as the best-protected platform. What should be counted is not a ratio, it is the number of blind spots left.

The indirect visibility column carries a cost too, and that cost does not show up in the table. When an investigation starts from an indirect record, it has to go to the machine and look for the answer: the record says a session was opened, not which script ran in that session. A record that names the item by name is answered with a query; an indirect record is answered with a site visit.

Opening the two closed sources makes 1,424 pairs visible by name. The cost has two parts: 5 configuration items per role profile — 20 across the fleet, because the setting is written per profile, not per machine — and 10,269,760 extra records a day. Total volume rises from 1.98 million to 12.25 million, a 6.2x increase. This line also sets up the question for this topic’s later lessons: volume is not just a number on its own, it means retention time and search cost.

The remaining 250 pairs cannot be closed. All of them are the same type — the firmware startup entry — and on the 250 machines that carry no management plane audit log, no source sees it. Making this type visible is not a logging setting, it is a management-architecture decision: the endpoints need to be connected to a centrally managed plane. Making an unclosable point at least visible is this course’s rule, and here even that cannot be done; the only option left is comparing that type from the outside with a periodic integrity measurement.

Summary

  • The three sources open by default on every machine write three of the twelve foothold point types by name; the only source that writes eight types by name is the process creation log, which is closed by default.
  • A record falls into one of three states: full visibility, which writes the item by name; indirect visibility, which shows the event without naming the item; and a blind spot, where no record exists at all. The authentication log sees no type fully, and sees two indirectly.
  • Across the fleet, 932 of 3,078 (machine, type) pairs are visible by name under the default configuration and 1,092 are in no source at all; once the two closed sources are opened, the visible-by-name count rises to 2,356 and the blind count falls to 250.
  • The cost is 5 configuration items per role profile and 10.2 million extra records a day; total volume rises to 6.2x.
  • All of the remaining 250 blind pairs are the firmware startup entry and cannot be seen from inside the endpoint; the bootloader boot entry, meanwhile, is never named in any source — it leaves only an indirect trace.

Next Step

In this lesson’s coverage table, the authentication log looks like the weakest source because it does not write a single type by name. But that source is looking at a different question: not the item being installed, but the account being taken over. The next lesson takes that log on its own and runs a form of experiment on a real fictional record set: it parses failed-login patterns to ask how many events a chosen threshold misses, how many times it raises an alert needlessly, and why a threshold counted per account goes completely silent.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close