---
title: 'Continuous Configuration Audit'
source: 'https://academia.sh/en/courses/runtime-security/continuous-configuration-audit'
course: 'Runtime Security and Secrets Management'
language: en
updated: '2026-10-07T04:31:06+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Runs the findings of continuous audit through the ownership resolution chain and counts separately the findings that fall to an application team, those that stay with the platform and those that are unowned'
    - 'Runs the queue for twelve weeks together with capacity and measures why the unowned finding does not shrink'
    - 'Ties the scope of the ownership levers and the stage at which the findings can be closed to numbers'
---

# Continuous Configuration Audit

The 147 open findings of continuous audit are run through the ownership resolution chain: the owner label resolves 65 of them, namespace ownership 24 and repository path mapping 18, and 40 (27.2%) remain unowned. 81 of the findings fall to application teams and 26 stay with the platform; because the capacity of the 40 unowned findings is zero, the queue grows to 112 in twelve weeks while the backlog of the fixing teams shrinks. 31 of the unowned findings are on resources outside the cluster, and no cluster mechanism can resolve them; 122 of the 147 findings could have been closed at an earlier stage, but for 25 of the 40 unowned findings there is no earlier stage.

The gate of the previous lesson saw only the object that was sent, and all ten rules read the declared
fields of the object. Even if the gate is tightened today, objects accepted yesterday stay where they
are; storage, network and identity definitions outside the cluster pass through no admission gate at
all. Only a continuously running audit scans that area.

This lesson measures that audit, but not the number of findings. Which misconfiguration closes at
which gate was measured in the **Cloud and Virtualization Security** course and is not repeated here.
The measure here is a single question: **whose job does a finding fall to.** How many findings return
to an application team, how many stay with the platform team and how many have no owner at all. An
unowned finding is different from an unmeasured finding: it is visible on the dashboard, it enters
the count, it is reported, and no one closes it.

- **IP18.** The audit scans the whole of the fictional regional measurement network: cluster objects
  and the storage, network, identity and registry resources outside the cluster. The finding
  inventory is a model built with `node`.
- **IP19.** The content of the rule set is not measured; the numbers are read from the open finding
  inventory. As long as the same resource violates the same rule it is counted as a single finding.
- **IP20.** The ownership resolution chain has three steps and they are tried in order: the owner
  label of the resource, the namespace ownership table, the repository path mapping. The first
  matching step wins; if none matches the finding is unowned.
- **IP21.** At which step a finding is resolved and to which team it falls is given in the model. In
  production this is read from the actual state of the inventory that day and changes over time.
- **IP22.** Capacity is fixed per week and counts only the item set aside for these findings. The queue
  runs by `max(0, queue + new - capacity)`; the difference in how hard the findings are to close is not
  modeled.

## The Finding Inventory and the Ownership Chain

```js
// finding.mjs — the open finding inventory of continuous audit and the ownership chain. IT IS A MODEL.
// [name, resource type, open findings, severity, step where ownership resolves, team, earlier closing stage]
export const FINDINGS = [
  ['no pod resource limit', 'cluster', 18, 'medium', 'label', 'subscriber', 'deploy'],
  ['root filesystem not read-only', 'cluster', 14, 'medium', 'label', 'field', 'deploy'],
  ['secret in environment variable', 'cluster', 6, 'high', 'label', 'billing', 'code'],
  ['mutable image tag', 'cluster', 9, 'low', 'namespace', 'subscriber', 'build'],
  ['unsigned image', 'registry', 7, 'high', 'namespace', 'field', 'build'],
  ['broadly accessible storage bucket', 'storage', 11, 'high', 'label', 'measurement', 'design'],
  ['unencrypted backup volume', 'storage', 5, 'high', 'repo', 'platform', 'design'],
  ['open management port', 'network', 9, 'high', 'none', '-', 'design'],
  ['unused identity definition', 'identity', 16, 'low', 'none', '-', 'none'],
  ['overly permissive role binding', 'identity', 8, 'high', 'namespace', 'platform', 'design'],
  ['resource with logging disabled', 'network', 13, 'low', 'repo', 'platform', 'deploy'],
  ['outdated runtime version', 'cluster', 16, 'medium', 'label', 'measurement', 'build'],
  ['old namespace left without an owner', 'cluster', 9, 'medium', 'none', '-', 'none'],
  ['hand-created network rule', 'network', 6, 'medium', 'none', '-', 'deploy'],
];

export const CHAIN = ['label', 'namespace', 'repo'];
export const STEP_NAME = { label: 'owner label', namespace: 'namespace ownership', repo: 'repo path mapping' };
// The item a team sets aside for security findings per week; the unowned finding has no capacity.
export const CAPACITY = { subscriber: 4, billing: 2, field: 3, measurement: 5, platform: 3, unowned: 0 };
export const NEW_PER_WEEK = 22; // new findings per week; tied to the deployment rate, given in the model

export const side = (b) => (b[4] === 'none' ? 'unowned' : b[5] === 'platform' ? 'platform' : 'application');
export const bucket = (b) => (b[4] === 'none' ? 'unowned' : b[5]);
export const sum = (f) => FINDINGS.filter(f).reduce((a, b) => a + b[2], 0);
export const gen = (B) => (h) => h.map((v, j) => j ? String(v).padStart(B[j]) : String(v).padEnd(B[0])).join('');
```

The three steps of the chain read from three different places and all three are an **inventory**
decision: has a label been put on the resource, is the namespace tied to a team, does the file that
defines the resource sit under a repository path. None of them is a decision of the audit itself.

## Whose Job the Finding Falls To

```js
// run1.mjs — the ownership chain is run: whose job does the finding fall to
import { FINDINGS, CHAIN, STEP_NAME, side, sum, gen } from './finding.mjs';

const SEV = ['high', 'medium', 'low'];
const TOTAL = sum(() => true);
console.log(`${FINDINGS.length} finding classes; open findings ${TOTAL} ` +
  `(${SEV.map((s) => `${s} ${sum((b) => b[3] === s)}`).join(' / ')})\n`);

const hs = gen([22, 8, 12, 9]);
console.log(hs(['resolution step', 'findings', 'cumulative', 'share']));
let k = 0;
for (const a of CHAIN) {
  k += sum((b) => b[4] === a);
  console.log(hs([STEP_NAME[a], sum((b) => b[4] === a), k, `${(100 * k / TOTAL).toFixed(1)}%`]));
}
console.log(hs(['unresolved', TOTAL - k, TOTAL, `${(100 * (TOTAL - k) / TOTAL).toFixed(1)}%`]));

const ht = gen([22, 10, 9, 8, 9]);
console.log('\n' + ht(['who gets the finding', 'findings', ...SEV]));
for (const [name, t] of [['application teams', 'application'], ['platform team', 'platform'], ['unowned', 'unowned']])
  console.log(ht([name, sum((b) => side(b) === t), ...SEV.map((s) => sum((b) => side(b) === t && b[3] === s))]));
const un = FINDINGS.filter((b) => side(b) === 'unowned');
console.log(`unowned findings in ${un.length} classes: ` + un.map((b) => `${b[0]} (${b[2]})`).join(', '));
```

```
14 finding classes; open findings 147 (high 46 / medium 63 / low 38)

resolution step       findings  cumulative    share
owner label                 65          65    44.2%
namespace ownership         24          89    60.5%
repo path mapping           18         107    72.8%
unresolved                  40         147    27.2%

who gets the finding    findings     high  medium      low
application teams             81       24      48        9
platform team                 26       13       0       13
unowned                       40        9      15       16
unowned findings in 4 classes: open management port (9), unused identity definition (16), old namespace left without an owner (9), hand-created network rule (6)
```

The first step of the chain is the strongest: the owner label alone finds the owner of sixty-five
findings, forty-four percent of the inventory. The second and third steps resolve twenty-four and
eighteen findings more. The three steps together reach seventy-three percent and stop there. The
remaining forty findings are items the audit sees but cannot send to anyone.

The lower table gives the real distribution. Eighty-one findings return to application teams, twenty-six
stay with the platform team and forty are unowned. The severity columns separate these three buckets
from each other: half of the platform team's findings are high severity, and the majority of the
application teams' findings are medium. The unowned bucket holds nine high severity findings, and all
nine are open management ports. This shows that being unowned is not limited to low severity items —
the reason a finding is unowned is not that it is unimportant but that the resource has not been tied
to anyone in the inventory.

## If the Queue Runs for Twelve Weeks

```js
// run2.mjs — the twelve-week course of the queue, the source of unownedness and the early stage
import { FINDINGS, CAPACITY, NEW_PER_WEEK, bucket, sum, gen } from './finding.mjs';

const TOTAL = sum(() => true), WEEKS = 12;
const hq = gen([12, 7, 15, 10, 14, 11]);
console.log(hq(['bucket', 'open', 'new per week', 'capacity', 'net per week', `week ${WEEKS}`]));
const last = {};
for (const bk of Object.keys(CAPACITY)) {
  const open = sum((b) => bucket(b) === bk), added = NEW_PER_WEEK * open / TOTAL, net = added - CAPACITY[bk];
  let q = open;
  for (let w = 0; w < WEEKS; w++) q = Math.max(0, q + net);
  last[bk] = Math.round(q);
  console.log(hq([bk, open, added.toFixed(1), CAPACITY[bk], net.toFixed(1), last[bk]]));
}
const owned = Object.keys(CAPACITY).filter((k) => k !== 'unowned');
const cap = owned.reduce((a, k) => a + CAPACITY[k], 0);
const unOpen = sum((b) => bucket(b) === 'unowned'), need = NEW_PER_WEEK * unOpen / TOTAL + unOpen / WEEKS;
console.log(`total backlog ${TOTAL} -> ${Object.values(last).reduce((a, b) => a + b, 0)}; owned buckets ` +
  `${TOTAL - unOpen} -> ${owned.reduce((a, k) => a + last[k], 0)}, unowned bucket ${unOpen} -> ${last.unowned}`);
console.log(`clearing the unowned bucket in ${WEEKS} weeks takes ${need.toFixed(1)} items a week: ` +
  `${(need / Math.max(...owned.map((k) => CAPACITY[k]))).toFixed(1)} times the capacity of the largest single team, ` +
  `${(100 * need / cap).toFixed(0)}% of the total capacity of the five teams (${cap})`);

const un = (f) => sum((b) => bucket(b) === 'unowned' && f(b));
const inside = un((b) => b[1] === 'cluster'), allUn = un(() => true), high = un((b) => b[3] === 'high');
console.log(`\nof the ${allUn} unowned findings ${inside} are in cluster objects, ${allUn - inside} in resources outside the cluster ` +
  `(${[...new Set(FINDINGS.filter((b) => bucket(b) === 'unowned' && b[1] !== 'cluster').map((b) => b[1]))].join(', ')})`);

const LEVERS = [['namespace ownership row', inside, high - un((b) => b[1] === 'cluster' && b[3] === 'high'), 'only cluster objects'],
  ['owner field in resource inventory', allUn, 0, 'all resources'],
  ['muting the low severity class', un((b) => b[3] === 'low'), high, 'finding disappears, resource stays'],
  ['owner label at the admission gate', 0, high, 'only newly created resources']];
const hl = gen([34, 10, 11, 16, 35]);
console.log('\n' + hl(['lever', 'resolved', 'remaining', 'remaining high', 'scope']));
for (const [name, c, h, scope] of LEVERS) console.log(hl([name, c, allUn - c, h, scope]));
console.log(`the admission gate does not touch the existing ${allUn} findings; it lowers the cluster share of weekly new unowned ` +
  `from ${(NEW_PER_WEEK * inside / TOTAL).toFixed(1)} to 0`);

const STAGES = ['design', 'build', 'code', 'deploy', 'none'];
const hg = gen([14, 10, 9]);
console.log('\n' + hg(['earlier stage', 'findings', 'unowned']));
for (const s of STAGES) console.log(hg([s, sum((b) => b[6] === s), un((b) => b[6] === s)]));
const early = sum((b) => b[6] !== 'none');
console.log(`${early} findings (${(100 * early / TOTAL).toFixed(0)}%) could have been closed at an earlier stage; ` +
  `for ${un((b) => b[6] === 'none')} of the ${allUn} unowned findings there is no earlier stage`);
```

```
bucket         open   new per week  capacity  net per week    week 12
subscriber       27            4.0         4           0.0         27
billing           6            0.9         2          -1.1          0
field            21            3.1         3           0.1         23
measurement      27            4.0         5          -1.0         15
platform         26            3.9         3           0.9         37
unowned          40            6.0         0           6.0        112
total backlog 147 -> 214; owned buckets 107 -> 102, unowned bucket 40 -> 112
clearing the unowned bucket in 12 weeks takes 9.3 items a week: 1.9 times the capacity of the largest single team, 55% of the total capacity of the five teams (17)

of the 40 unowned findings 9 are in cluster objects, 31 in resources outside the cluster (network, identity)

lever                               resolved  remaining  remaining high                              scope
namespace ownership row                    9         31               9               only cluster objects
owner field in resource inventory         40          0               0                      all resources
muting the low severity class             16         24               9 finding disappears, resource stays
owner label at the admission gate          0         40               9       only newly created resources
the admission gate does not touch the existing 40 findings; it lowers the cluster share of weekly new unowned from 1.3 to 0

earlier stage   findings  unowned
design                33        9
build                 32        0
code                   6        0
deploy                51        6
none                  25       25
122 findings (83%) could have been closed at an earlier stage; for 25 of the 40 unowned findings there is no earlier stage
```

The upper table sends six buckets in different directions. The capacity of the billing and measurement
teams is above the incoming load, so their backlog shrinks: billing goes to zero in twelve weeks,
measurement goes from twenty-seven to fifteen. The subscriber team stands in exact balance. The field
and platform teams fall a little behind every week, and the platform's backlog rises from twenty-six
to thirty-seven. All of these are manageable numbers, and what they have in common is that each has an
owner.

The sixth row is something else. In the unowned bucket capacity is zero, because capacity is a concept
that belongs to a team and this bucket has no team. The forty findings receive six new findings every
week, none ever closes, and in twelve weeks they rise to one hundred twelve. While the total
backlog of the five buckets that have an owner falls from one hundred seven to one hundred
two in the same period, the total inventory rises from one hundred forty-seven to two hundred
fourteen: the whole of the increase comes from a single bucket, and that bucket responds to no
fixing effort.

The last line writes the price of this. Clearing the unowned bucket in twelve weeks takes a capacity of
nine point three items a week. This number is one point nine times the capacity of the largest single
team and fifty-five percent of the **total** capacity of the five teams. That is, if the unowned
findings were handed over to one team in bulk, that team's whole capacity would not be enough; not even
the capacity of the two largest teams would be enough, the share of a third team would be needed, and
those teams would have to give up their own findings. The cost of being unowned is not that no one
looks, but that the number to be paid if someone were to look is this large.

## Who Will Give the Ownership

The middle block separates where the unownedness comes from: of the forty findings only nine are in a
cluster object, and thirty-one are in network and identity resources. This separation determines the
whole of the lever table. Writing a row into the namespace ownership table resolves nine findings and
leaves thirty-one in place — because those resources are not in a namespace. The owner label rule added
to the admission gate of the previous lesson touches **none** of the existing forty findings; it affects
only newly created cluster objects and lowers the cluster share of weekly new unowned from one point
three to zero. Muting the low severity class lowers the inventory from forty to twenty-four but never
touches the remaining nine high severity findings; the resource stays where it is, it only becomes
invisible.

The only row that resolves all forty of the forty is the second: an owner field in the resource
inventory. That field is not a mechanism of the cluster and not an output of the audit either — it is
metadata filled in when the resource is created. Continuous audit cannot **produce** ownership; it
can only report its absence.

## Where It Would Have Closed Earlier

Looked at with two columns, the last table gives the result of the lesson. One hundred twenty-two
of the hundred forty-seven findings — eighty-three percent — could have been closed at an earlier
stage: fifty-one with a manifest check at the deploy stage, thirty-three with an access decision at
design, thirty-two with a rebuild at the build stage, six in code. Continuous audit finds these one
hundred twenty-two again in every scan and reports them again in every scan.

The second column reads in the opposite direction. For twenty-five of the forty unowned findings an
earlier stage **does not exist**, and this is not a coincidence: unused identity definitions and the
old namespace left without an owner are resources that were born at some stage and forgotten. The
decision that produced them belongs not to a stage but to a record that was never kept. The measure of
this course — how many times cheaper it is to close an item earlier — takes another form here:
ownership is not an item that can be closed at a stage but a field the resource has to carry from the
first moment it exists. A finding that appears when the field is left empty becomes cheaper at no
stage; it merely accumulates in a list that no one looks at.

## Summary

- The ownership of 147 open findings is resolved by a three-step chain: owner label 65, namespace
  ownership 24, repository path mapping 18. The chain stops at 72.8% and 40 findings (27.2%) remain
  unowned.
- 81 of the findings fall to application teams and 26 stay with the platform team. The unowned bucket
  has 9 high severity findings; being unowned is not limited to low severity.
- The capacity of the unowned bucket is zero: while the 40 findings rise to 112 in twelve weeks, the
  total backlog of the five owned buckets falls from 107 to 102. Clearing this bucket in the same
  period takes 9.3 items a week, that is a share of 55% of the total capacity of the five teams.
- 31 of the 40 unowned findings are in network and identity resources outside the cluster; namespace
  ownership resolves only 9 of them, the admission gate touches none of the existing ones, and muting
  makes 16 of them invisible and leaves the resource in place. The only thing that resolves all forty
  is an owner field in the resource inventory.
- 122 of the 147 findings (83%) could have been closed at an earlier stage; for 25 of the 40 unowned
  findings there is no earlier stage, because ownership is a decision of resource creation and not of
  a stage.

## Next Step

In the earlier stage column thirty-two findings read **build**: the mutable image tag, the unsigned
image and the outdated runtime version. What these three classes have in common is that none of them
can be fixed in place — to close them a new image has to be built and thirty-one replicas redeployed.
The audit finds them again in every scan, because the distance between scan and fix is not a
configuration change but a release cycle. The next lesson measures that cycle: after a fix is
published, how many days does it take to reach the replicas, what is the difference in days between
in-place patching and rebuilding, and how many replicas does the disruption that rebuilding brings in
return touch.
