Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

OBOM: inventory what really runs in production

By updated

Improve this page

An OBOM (Operations Bill of Materials) is the inventory of what really runs: dynamically loaded libraries, modules enabled depending on the environment, active configuration, connected services. It complements the SBOM (Software Bill of Materials, the machine-readable inventory of what was assembled and shipped) without replacing it. This lesson is for a DevOps engineer who already knows how to produce an SBOM: it shows why the build is not enough, how to collect the runtime state on Linux, in containers and in Kubernetes, and how to correlate the two inventories to spot "ghost" components.

The typical scenario is a critical CVE (a publicly catalogued vulnerability) landing on a library. The SBOM says "we don't use this lib", yet in production a dynamically loaded plugin uses it. The SBOM described what had been assembled, not the observed operational state.

What you will learn

  • Explain the gap between a build-time SBOM and the state in production.
  • Collect the runtime information of a Linux process and of a container, with /proc, lsof and language introspection.
  • Generate an inventory with cdxgen and place the limits of its OBOM support.
  • Correlate SBOM and OBOM with a comparison script.
  • Recognise what an OBOM does not guarantee, and its place among the inventories.
  • Anchor the SBOM/OBOM correlation in the Socle framework: no requirement names the OBOM, but the SBOM completeness check (no ghost component) of SOCLE-PKG-SBM-3 (in French) is where it serves as evidence.

What are the prerequisites?

You need to know the SBOM and the family of inventories described in xBOM: beyond the SBOM, and be able to read the output of a Linux command. The examples touch containers and Kubernetes; the SBOM generator Syft (in French) serves as a point of comparison.

SBOM vs OBOM: the build/runtime gap

The two inventories are not produced at the same moment or from the same source, and that is exactly where the gap comes from. The SBOM reads declaration files at the moment the software is assembled; the OBOM queries running processes. Between the two, there was a deployment, environment variables, enabled plugins and system libraries provided by the host.

What the SBOM captures... and what it misses

Read this table row by row as a list of questions that an SBOM alone cannot answer. The right-hand column states each time what extra information observing the runtime brings.

AspectSBOM (build-time)OBOM (runtime)
MomentAt compilation/packagingIn production
SourceManifests (package.json, go.mod, etc.)Running processes
DependenciesDeclared and transitiveActually loaded
ConfigurationDefault valuesActive configuration
Optional modulesListed as optionalEnabled or not
PluginsPotentialActually loaded

A concrete example: a Python application

The code below picks its database driver from an environment variable and loads its plugins by walking a folder. No static analysis tool (one that reads the code without running it) can guess what will really be imported: the decision is taken at startup, on the production machine.

# app.py - Application with dynamic loading
import os
import importlib
# The database driver depends on the environment
db_driver = os.getenv("DB_DRIVER", "sqlite")
db_module = importlib.import_module(f"databases.{db_driver}")
# Plugins are loaded from a folder
for plugin in os.listdir("plugins/enabled"):
importlib.import_module(f"plugins.{plugin}")

What the SBOM sees (inventory produced from the build or from the packaged environment):

flask==3.0.0
databases-sqlite==0.9.0
databases-postgres==0.9.0 # extras dependency
plugin-analytics==1.2.0 # available plugin
plugin-cache==2.0.0 # available plugin

Note: this example is simplified for teaching purposes. In reality, the sources of divergence are more varied (wheels, entry points, runtime mounts, etc.).

What runs in production (real runtime):

flask==3.0.0
databases-postgres==0.9.0 # ← This one is enabled
plugin-cache==2.0.0 # ← Only this plugin is enabled
psycopg2==2.9.9 # ← Transitive dependency of postgres
libpq.so.5 # ← C library loaded by psycopg2

The SBOM lists databases-sqlite (the default) but production uses databases-postgres. If a CVE hits psycopg2, a build-time SBOM may not be enough to say whether this dependency is really active in this environment. The OBOM then helps tell theoretical presence from actual use.

What the OBOM captures

An OBOM is not limited to a list of packages: it describes a working state. The useful information falls into five families, some of which are obtained with one command while others need extra enrichment work. Knowing which one is easy to collect determines the order in which you will build your inventory.

The 5 categories of runtime information

The diagram groups these families and gives, for each, the type of source to query.

The 5 categories of information captured by an OBOM: loaded libraries, enabled modules, active configuration, connected services and system state, each with its typical sources.Contents of an OBOM① Loaded libraries.so, .dll, .dylib, mapped memory, dlopen()② Enabled modulesplugins, runtime extensions, feature flags③ Active configurationconfig files, env variables, mounted secrets④ Connected servicesdatabases, external APIs, message queues⑤ System statekernel version, system packages, CA certificates
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Detail by category

The right-hand column is the most useful: it indicates the type of incident each category helps settle, from a CVE in a C library to a lowered TLS setting in production.

CategoryExamplesWhy it matters
Loaded librarieslibssl.so.3, libc.so.6, libpq.so.5CVEs in C/system libs
Enabled modulesWordPress plugins, Python extensionsActive code not visible at build
Active configurationTLS_MIN_VERSION=1.2, DEBUG=falseEffective security settings
Connected servicespostgres://db:5432, redis://cache:6379Network dependencies
System stateKernel 5.15, glibc 2.35System vulnerabilities

Collecting runtime information on Linux

Linux exposes the state of a process in the /proc pseudo-filesystem (a virtual directory the kernel fills in on the fly), which lets you query it without instrumenting the application. The four methods that follow go from the most generic to the most specific: shared libraries, Python modules, Node.js packages, then environment variables. None is enough on its own, they complement each other. All of them require at least the right to read /proc for the account that owns the process.

Method 1: shared libraries through /proc

The most direct method to list the libraries loaded by a process:

Terminal window
# List the libraries loaded by a process
cat /proc/$(pgrep -x python3)/maps | grep '\.so' | awk '{print $6}' | sort -u

The output looks like this (paths given as an illustration):

/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libpthread.so.0
/lib/x86_64-linux-gnu/libm.so.6
/usr/lib/x86_64-linux-gnu/libssl.so.3
/usr/lib/x86_64-linux-gnu/libcrypto.so.3
/usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so

On a python:3.12-slim container (python3 process as PID 1, read with docker exec … cat /proc/1/maps), the same chain listed for example /usr/lib/x86_64-linux-gnu/libssl.so.3, /usr/lib/x86_64-linux-gnu/libcrypto.so.3 and /usr/local/lib/python3.12/lib-dynload/_ssl.cpython-312-x86_64-linux-gnu.so: paths vary depending on the image. These are .so file names, not package names: the section on correlation shows how to match them to an SBOM.

collect-runtime-libs.sh
#!/bin/bash
# Collects the libraries loaded by a process
PID=${1:-}
if [ -z "$PID" ]; then
echo "Usage: $0 <PID>"
exit 1
fi
echo "=== Runtime libraries for PID $PID ==="
echo ""
# Process name
PROC_NAME=$(cat "/proc/$PID/comm")
echo "Process: $PROC_NAME"
echo ""
# Unique .so libraries
echo "Shared libraries:"
grep -E '\.so(\.[0-9]+)*$' "/proc/$PID/maps" | \
awk '{print $6}' | \
sort -u | \
while read -r lib; do
# Print the library path
if [ -f "$lib" ]; then
echo " - $lib"
fi
done
# Open files (fd)
echo ""
echo "Open files (non-trivial):"
for fd in "/proc/$PID/fd"/*; do
cible=$(readlink "$fd" 2>/dev/null) || continue
case "$cible" in
pipe:*|socket:*|anon_inode:*|/dev/*) continue ;;
esac
echo " $cible"
done

These commands depend on pgrep, which returns one PID per process carrying the requested name. With two python3 processes, the substitution produces two numbers and cat /proc/<pid1> <pid2>/maps fails: add -o to keep the oldest (pgrep -o -x python3), or pass the PID by hand. Another trap: lsof -p PID -i does an OR between the two filters and lists the connections of all processes on the machine, hence the -a in the last command. Finally, pgrep and lsof are not installed in the Python slim images.

Method 2: loaded Python modules

For Python applications, list the modules actually imported:

runtime_modules.py
import sys
import json
def get_loaded_modules():
"""Return the loaded Python modules with their path."""
modules = {}
for name, module in sys.modules.items():
if hasattr(module, '__file__') and module.__file__:
modules[name] = {
"path": module.__file__,
"version": getattr(module, '__version__', None)
}
return modules
if __name__ == "__main__":
print(json.dumps(get_loaded_modules(), indent=2))

Example run through kubectl exec:

Terminal window
# Via a shell in the container
kubectl exec -it myapp-pod -- python -c "
import sys, json
mods = {k: getattr(v, '__file__', 'builtin') for k, v in sys.modules.items()}
print(json.dumps(mods, indent=2))
"

Method 3: loaded Node.js packages

For Node.js applications:

runtime-deps.js
// Run on demand in the container (not through a debug endpoint in production)
function getLoadedModules() {
const modules = {};
for (const [path, mod] of Object.entries(require.cache)) {
if (path.includes('node_modules')) {
const parts = path.split('node_modules/');
const pkgPath = parts[parts.length - 1].split('/')[0];
if (!modules[pkgPath]) {
try {
const pkg = require(`${pkgPath}/package.json`);
modules[pkgPath] = pkg.version;
} catch {
modules[pkgPath] = 'unknown';
}
}
}
}
return modules;
}
console.log(JSON.stringify(getLoadedModules(), null, 2));

Method 4: active environment variables

Environment variables often define runtime behaviour. The commands below keep only the name of each variable (cut -d= -f1 cuts at the = sign), never its value, which may be a secret:

Terminal window
# List the environment variables of a process
cat /proc/$(pgrep -x python3)/environ | tr '\0' '\n' | grep -E '^(DB_|API_|FEATURE_|DEBUG|LOG_LEVEL)' | cut -d= -f1
# In a Kubernetes container
kubectl exec myapp-pod -- env | grep -E '^(DB_|API_|FEATURE_)' | cut -d= -f1

Collecting information in containers

In a container, the same logic applies but from the outside: docker exec opens a process in the container's namespaces (its isolated view of processes and files) and gives access to its /proc. Two angles are possible and do not give the same result: query the active processes, or export the filesystem to analyse it with a composition tool. The second is more complete on the installed packages, the first is the only one that sees what is really loaded.

Scanning a running container

These commands assume that the queried utilities exist in the image; on a distroless image (a minimal image with no shell or system tools) or one built FROM scratch, neither ps nor netstat will be present.

Terminal window
# List the processes in a container
docker exec mycontainer ps aux
# Libraries loaded by the main process (PID 1 in the container)
docker exec mycontainer cat /proc/1/maps | grep '\.so' | awk '{print $6}' | sort -u
# Active network connections
docker exec mycontainer netstat -tuln
# Installed packages (Debian/Ubuntu)
docker exec mycontainer dpkg-query -W -f='${Package} ${Version}\n'
# Installed packages (Alpine)
docker exec mycontainer apk list --installed

Scanning the filesystem of a running container

Syft can scan a Docker image, but also the exported filesystem of a container:

Terminal window
# Scan the image (classic SBOM)
syft myregistry/myapp:v1.0.0 -o cyclonedx-json > sbom.json
# Scan the rootfs of a running container
CONTAINER_ID=$(docker ps -qf "name=myapp")
docker export "$CONTAINER_ID" -o rootfs.tar
syft file:rootfs.tar -o cyclonedx-json > runtime-sbom.json

Syft does not read its input from a pipe: docker export … | syft /dev/stdin fails (unable to get absolute path for analysis path="/dev/stdin"), because the path points to a pipe and not to a file. The intermediate archive rootfs.tar is therefore necessary. The syft file:rootfs.tar command unpacks it and catalogues the system and Python packages, then writes a CycloneDX document (an SBOM format maintained by OWASP) usable with the comparison script further down. Delete rootfs.tar afterwards: it contains the whole filesystem of the container, secrets included.

OBOM collection script for a Docker container

This script assembles the two previous sources, system packages and loaded libraries, into a minimal CycloneDX 1.6 document. It is deliberately simple and serves as a starting point: the obom:collection-method property it writes in the metadata lets you tell this document apart from a classic SBOM.

collect-obom.sh
#!/bin/bash
# Partial collection prototype for a container
# Useful for demonstration or experimentation, insufficient as a production OBOM
CONTAINER=$1
OUTPUT=${2:-obom.json}
if [ -z "$CONTAINER" ]; then
echo "Usage: $0 <container_name_or_id> [output.json]"
exit 1
fi
echo "Collecting OBOM for container: $CONTAINER"
# Create the output JSON
cat > "$OUTPUT" << EOF
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"metadata": {
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"component": {
"name": "$CONTAINER",
"type": "container"
},
"properties": [
{"name": "obom:collection-method", "value": "runtime-inspection"}
]
},
"components": [
EOF
# Collect the system packages
echo " Collecting system packages..."
docker exec "$CONTAINER" sh -c '
if command -v dpkg-query >/dev/null; then
dpkg-query -W -f "\${Package}\t\${Version}\n" 2>/dev/null
elif command -v apk >/dev/null; then
apk list --installed 2>/dev/null | cut -d" " -f1 | sed "s/-\([0-9]\)/ \1/"
elif command -v rpm >/dev/null; then
rpm -qa --queryformat "%{NAME}\t%{VERSION}\n" 2>/dev/null
fi
' | while read -r name version; do
echo " {\"type\": \"library\", \"name\": \"$name\", \"version\": \"$version\", \"properties\": [{\"name\": \"source\", \"value\": \"os-package\"}]},"
done >> "$OUTPUT"
# Collect the libraries loaded by the main process
# Note: PID 1 is not always the application process (it can be an init/wrapper)
echo " Collecting loaded libraries..."
docker exec "$CONTAINER" cat /proc/1/maps 2>/dev/null | \
grep -E '\.so(\.[0-9]+)*$' | \
awk '{print $6}' | \
sort -u | \
while read -r lib; do
name=$(basename "$lib" | sed 's/\.so.*//')
echo " {\"type\": \"library\", \"name\": \"$name\", \"properties\": [{\"name\": \"source\", \"value\": \"loaded-library\"}, {\"name\": \"path\", \"value\": \"$lib\"}]},"
done >> "$OUTPUT"
# Close the JSON (remove the last comma and close)
sed -i '$ s/,$//' "$OUTPUT"
echo " ]" >> "$OUTPUT"
echo "}" >> "$OUTPUT"
echo "OBOM written to: $OUTPUT"

Generating an OBOM with cdxgen

cdxgen is a CycloneDX generator tied to the OWASP/CycloneDX ecosystem. It offers useful capabilities to move toward the OBOM, notably to inventory a running Linux host, but its support must be validated depending on the context. Not all its commands produce an OBOM: the paragraph after the three examples states which ones do.

Installation

cdxgen is distributed as a global npm package and therefore needs a Node.js installation on the machine that runs the collection. The version given (13.4.0) is the one published on the npm registry at the time of writing; check it before use.

Terminal window
npm install -g @cdxgen/cdxgen@13.4.0

Generating an OBOM

The --deep option pushes the analysis beyond dependency manifests, but it is not enough to make an OBOM: everything depends on the target (-t) you inventory, project, image or host.

Terminal window
# Node.js project: deeper dependency analysis (still an SBOM)
cdxgen -t nodejs -o sbom-projet.json --deep
# Docker image: what is installed, not what is loaded (in-depth SBOM)
cdxgen -t docker -o sbom-image.json my-image:1.0.0 --deep
# Running host (type os, equivalent to the obom command): this is an OBOM
cdxgen -t os -o obom.json --deep

These three commands are not equal with regard to the thesis of this page, which defines the OBOM as the state observed while running and not as the list of what is installed. Only -t os on a running host comes close: the cdxgen documentation presents it as the way to get visibility into processes, network, services and startup (runtime state, for example listening ports), in addition to the installed software.

Conversely, -t docker on an image analyses frozen content: no container is running, so nothing is "loaded". You get what is installed in the image, that is, an in-depth SBOM. As for -t nodejs --deep, it looks for dependencies more thoroughly but remains a project analysis. Even for -t os, the cdxgen documentation does not state, at the time of writing (October 2026), whether kernel modules or the libraries loaded by each process are covered: compare the result with /proc/<pid>/maps before using it as evidence.

Kubernetes: collecting the OBOM of all pods

At cluster scale, collection runs into two constraints: kubectl exec must be allowed by the RBAC (role-based access control) of the account used, and each container of a pod must be processed separately. Also plan for pods without a shell: the command will fail silently and produce an empty file, which must be read as an absence of data and not as an absence of components.

Cluster-wide collection script

The script walks all the pods of a namespace and writes three files per container: packages, libraries and network connections.

collect-cluster-obom.sh
#!/bin/bash
# Collects the runtime inventory of all pods
NAMESPACE=${1:-default}
OUTPUT_DIR=${2:-./obom-collection}
mkdir -p "$OUTPUT_DIR"
echo "Collecting OBOM for namespace: $NAMESPACE"
# List all pods
kubectl get pods -n "$NAMESPACE" -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].name}{"\n"}{end}' | \
while IFS=$'\t' read -r pod containers; do
for container in $containers; do
echo "Processing: $pod / $container"
# Collect the packages (if possible)
kubectl exec -n "$NAMESPACE" "$pod" -c "$container" -- sh -c '
if command -v dpkg-query >/dev/null; then
dpkg-query -W -f "\${Package}\t\${Version}\n"
elif command -v apk >/dev/null; then
apk list --installed 2>/dev/null
fi
' 2>/dev/null > "$OUTPUT_DIR/${pod}_${container}_packages.txt"
# Collect the loaded libraries
kubectl exec -n "$NAMESPACE" "$pod" -c "$container" -- \
cat /proc/1/maps 2>/dev/null | \
grep -E '\.so' | awk '{print $6}' | sort -u \
> "$OUTPUT_DIR/${pod}_${container}_libs.txt"
# Collect the network connections
kubectl exec -n "$NAMESPACE" "$pod" -c "$container" -- \
netstat -tuln 2>/dev/null \
> "$OUTPUT_DIR/${pod}_${container}_network.txt"
done
done
echo "Collection complete. Files in: $OUTPUT_DIR"

With specialised tools

These tools have different roles, do not confuse them. eBPF (extended Berkeley Packet Filter) is a Linux kernel mechanism that lets an agent observe system calls and processes without modifying the application; the table below mentions it for Sysdig.

ToolCategoryFunction
TrivyComposition scanAnalyses image/filesystem/k8s, not a fine-grained runtime inventory
SyftComposition scanSBOM from images/filesystems
SysdigRuntime telemetryRuntime observation through an eBPF agent
FalcoBehavioural detectionAlerts on abnormal behaviour, not an OBOM
RAD Security (formerly KSOC)Kubernetes visibilityVisibility into running Kubernetes workloads

Correlating SBOM and OBOM

Collecting the two inventories only matters if you compare them. Three gaps deserve investigation: a component present at runtime but absent from the SBOM, a version that differs between the two documents, and a component declared but never loaded. The first case is the most serious: it signals code that runs without having gone through your build chain.

Correlation workflow

The diagram summarises the sequence, from the production of the two documents to the identification of "ghost" components.

SBOM and OBOM correlation: the SBOM declares flask and redis; at runtime, the OBOM shows flask loaded, redis absent, and psycopg2 and libpq.so.5 loaded without appearing in the SBOM; the unified inventory sorts components into SBOM only (unused), OBOM only (to investigate) or both (active dependency).Unified inventorySBOM (build time)OBOM (runtime)SBOM onlyunusedOBOM onlyto investigateBothactive dependencyflask==3.0.0redis==5.0.0(no psycopg2)flask==3.0.0: presentredis: not loadedpsycopg2==2.9.9: missing from SBOMlibpq.so.5: missing from SBOMcorrelation
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Comparison script

The script expects two CycloneDX documents: the SBOM from Syft or cdxgen on one side, and on the other an OBOM produced by collect-obom.sh or by cdxgen -t os run on the running host (a cdxgen -t docker on an image gives an SBOM: obom_only would stay empty by construction). The text files of the cluster script are not suitable as they are, they are not CycloneDX. It sorts components into four sets: present on both sides, specific to the SBOM, specific to the OBOM and diverging versions. The obom_only category is the one that must trigger an analysis.

A reading trap: the OBOM lists .so files (libssl.so.3) where the SBOM lists packages (openssl). The script therefore applies a rough normalisation (removing the lib prefix and the .so.N suffix), which matches libpq.so.5 with pq but not with libpq5, nor libssl with openssl. Expect some noise in obom_only: sort it before concluding there is a ghost component, or attach each file to its package with dpkg -S <path> (Debian) or rpm -qf <path> (RHEL).

#!/usr/bin/env python3
"""compare-sbom-obom.py - Compare SBOM and OBOM to detect gaps."""
import json
import re
import sys
from collections import defaultdict
def normalise(name):
"""Match a .so file name with a package name (heuristic)."""
name = name.lower().rsplit("/", 1)[-1]
name = re.sub(r"\.so(\.[0-9]+)*$", "", name)
name = re.sub(r"^lib", "", name)
return name.replace("_", "-")
def load_bom(path):
"""Load a CycloneDX BOM and extract the components."""
with open(path) as f:
bom = json.load(f)
components = {}
for comp in bom.get("components", []):
name = normalise(comp.get("name", ""))
version = comp.get("version", "unknown")
components[name] = version
return components
def compare(sbom_path, obom_path):
"""Compare SBOM and OBOM, return the gaps."""
sbom = load_bom(sbom_path)
obom = load_bom(obom_path)
sbom_names = set(sbom.keys())
obom_names = set(obom.keys())
results = {
"sbom_only": sorted(sbom_names - obom_names), # Declared, not loaded
"obom_only": sorted(obom_names - sbom_names), # Loaded, not declared
"both": sorted(sbom_names & obom_names), # Match
"version_mismatch": [] # Different versions
}
# Check the versions
for name in results["both"]:
# A .so library has no version in the OBOM: ignore it
if obom[name] != "unknown" and sbom[name] != obom[name]:
results["version_mismatch"].append({
"name": name,
"sbom_version": sbom[name],
"obom_version": obom[name]
})
return results
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: compare-sbom-obom.py sbom.json obom.json")
sys.exit(1)
results = compare(sys.argv[1], sys.argv[2])
print("=== SBOM-only components (declared but not loaded) ===")
for name in results["sbom_only"][:10]:
print(f" - {name}")
print("\n=== OBOM-only components (loaded but not declared) ===")
for name in results["obom_only"][:10]:
print(f" [!] {name}")
print(f"\n=== Summary ===")
print(f"Match: {len(results['both'])}")
print(f"SBOM-only: {len(results['sbom_only'])}")
print(f"OBOM-only: {len(results['obom_only'])} ← To investigate")
print(f"Version mismatch: {len(results['version_mismatch'])}")

Usage:

Terminal window
python3 -I compare-sbom-obom.py sbom.json obom.json

Integrating with GUAC

GUAC (Graph for Understanding Artifact Composition; guide in French) can ingest CycloneDX and SPDX documents (SPDX being the other standard SBOM format) as well as other supply chain metadata, which makes it a good candidate to correlate SBOMs, attestations, vulnerabilities and, potentially, operational inventories modelled in a compatible way. The real depth of this correlation depends, however, on the quality and structure of the ingested documents.

Ingesting SBOM and OBOM into GUAC

guacone collect files accepts both documents without distinguishing their nature: it is you who give meaning to the whole by ingesting both, then querying the graph.

Terminal window
# Ingest the SBOM (build-time)
guacone collect files sbom.json
# Ingest the OBOM (runtime)
guacone collect files obom.json
# GUAC can now answer queries such as:
# "Which components are in production but not in the SBOM?"
# "Does this container use a lib vulnerable to this CVE?"

OBOM good practices

An OBOM is a snapshot: its value depends directly on its freshness and on the discipline with which it is produced. Two decisions structure the approach: how often to collect, and which data to forbid yourself from capturing. The second is not negotiable, an inventory that contains secrets becomes an asset to protect itself.

Collection frequency

The most useful trigger is not the calendar but the deployment: that is when the runtime state really changes.

EnvironmentFrequencyReason
ProductionDaily or after each deploymentFast drift detection
StagingAt each deploymentValidation before prod
Dev/TestOptionalLess critical

What to collect (and not to collect)

The rule fits in one sentence: collect identifiers, never values. The name of an environment variable documents the configuration, its value may be a password.

To collectNot to collect
Package namesValues of secrets
VersionsUser data
Library pathsContent of sensitive config files
Env variable namesValues of secret env variables
Network connections (hosts)Credentials in connections

Three triggers cover most needs and are set up with standard Kubernetes objects: a Job after deployment, a daily CronJob and a manual collection in response to an incident.

  1. At each deployment

    Generate an OBOM snapshot after the application starts. The cluster script relies on kubectl exec: it runs in a Job whose ServiceAccount has the create right on pods/exec (collect-obom.sh, based on docker exec, does not work from a pod). Files written to /tmp/obom disappear with the pod: mount a volume or send them to storage before the Job ends.

    # Kubernetes Job post-deploy
    apiVersion: batch/v1
    kind: Job
    metadata:
    name: collect-obom
    spec:
    template:
    spec:
    restartPolicy: Never
    serviceAccountName: obom-collector # RBAC: get/list pods, create pods/exec
    containers:
    - name: collector
    # Image you build: kubectl + collect-cluster-obom.sh,
    # pinned by digest
    image: obom-collector:1.2.0@sha256:<digest-of-the-image>
    command: ["./collect-cluster-obom.sh", "myapp", "/tmp/obom"]
  2. Daily

    Scheduled collection to detect drift (enabled plugins, updated libs):

    apiVersion: batch/v1
    kind: CronJob
    metadata:
    name: daily-obom
    spec:
    schedule: "0 2 * * *" # 2 a.m.
    jobTemplate:
    # ...
  3. On a security incident

    Immediate collection to answer "do we use this vulnerable lib?"

What an OBOM does not solve

The OBOM is a valuable complement, but you should not replace one myth ("the SBOM is enough") with another ("the OBOM settles the runtime").

What the OBOM does not guarantee

These six limits are structural: no improvement in tooling lifts them, because observing a system at a given moment says nothing about its origin or its future.

LimitationExplanation
Integrity of the build chainThe OBOM observes, it does not prove origin
Absence of future driftSnapshot at a time T, not continuous monitoring
Complete coverageDepends on the collection method and privileges
Exploitability of a CVEPresence ≠ exploitable vulnerability (that is the role of VEX, a document stating whether a product is really affected)
Compliance on its ownOne element among others in a security posture
Automatic detection of everythingHighly dynamic plugins/behaviours may escape it

The OBOM complements, it does not replace

The OBOM fits into a whole:

  • SBOM: what was assembled
  • Attestations: proof of provenance and integrity
  • VEX: exploitability of vulnerabilities
  • Runtime telemetry: continuous observation
  • OBOM: operational state observed at a time T

Maturity levels

Place yourself on this scale to plan your progression:

Level 1, Foundations

The starting point: you know what the shipped image contains, but nothing of what happens after it starts.

  • Final image SBOM
  • Vulnerability scan
  • OS package inventory

Level 2, Partial observation

You take runtime snapshots and compare them by hand: enough to answer a one-off question, too slow for a large incident.

  • Runtime snapshot (loaded libs, env variables)
  • Manual SBOM / snapshot comparison
  • Alerts on major gaps

Level 3, Correlation

Introspection goes down to the language modules and the comparison is automated, with a history that lets you date a drift.

  • Language introspection (Python/Node/Java)
  • Automated multi-source correlation
  • Vulnerability enrichment
  • Historised storage

Level 4, Driven decisions

The inventory feeds a queryable graph and risk decisions are taken from its answers, without prior manual collection. This level cannot be built before the previous one.

  • Structured and normalised OBOM
  • Continuous correlation (GUAC or equivalent)
  • Queryable knowledge graph
  • Risk decision built into the workflow

Key points

Six ideas to keep:

  • SBOM and OBOM are complementary, the SBOM describes what was assembled, the OBOM observes the real operational state
  • Several collection methods: /proc/<pid>/maps, language introspection, filesystem scan, each with its limits
  • The SBOM/OBOM correlation reveals the gaps, "ghost" components absent from the SBOM but active in production
  • The OBOM is not magic, it is a partial snapshot, not continuous monitoring nor proof of integrity
  • Automate the collection, after each deployment and daily, without ever collecting secrets
  • Expected evidence (SOCLE-PKG-SBM-3): a completeness check of the SBOM, here the SBOM/OBOM correlation report whose obom_only components have been sorted and explained

Knowledge check

Check that the essentials of this lesson are in place. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn