Developing the Environment and Workload Model
- Status: Implemented (environment identification, the common workload/container model, recording into state, and notifications. A single state file still holds a single environment — consolidating multiple environments into one state file remains future work.)
- Started: August 25, 2026
- Last updated: August 29, 2026
Purpose
Implement the capabilities deferred during the development of the image identity model:
- An environment identifier that serves as the unit of scan-history continuity
- Mapping containers to the services (workloads) they belong to
- Reworking scan history so that records from multiple hosts can be handled in one state file
What each model is for
Before the implementation details, this section lays out why each model exists.
Environment
An environment is the name a person gives to the scope one KestreLynx instance observes (today one Docker host's worth; in the future one Kubernetes cluster's worth), and it is the unit that ties scan history together. It is not host-identifying information: it is not derived from hostnames or IP addresses and plays no role in identifying them.
It has two jobs.
- Letting a person declare history continuity: rebuild or migrate the server and, as long as the same name stays in use, the history continues as the same environment
- Change the name to treat it as a different one
- Whether something is "the same environment" is the operator's intent, not something that can be decided mechanically from matching hostnames or IPs, so it is supplied as configuration
- Telling notifications apart: when several instances notify the same Slack channel, the header shows which environment a report belongs to
Which machine things actually run on (hostname, IP addresses, and so on) is a separate observed fact, and the environment name does not stand in for it.
Workload
A workload is the mapping of a container to the service it runs as part of. Vulnerabilities are detected per image, but the unit an operator reasons about is the service, so the workload exists to answer "which services does this image's CVE affect?". When several services run the same image, each of them is visibly affected.
Compose-launched containers reveal their workload through the project and service labels. When no mapping exists (a container started directly with docker run, for example), it is represented as unknown rather than guessed.
Container
A container entry is the observed fact of an actually running instance. It exists to tell apart multiple containers running the same image (or the same workload) in reports. Even when the role (workload) is unknown, the instance's name can still be reported as an observation.
Note that both scanning and history remain keyed by the image entity (the ContentID). Workload and container are an association layer for the people reading the results; they affect neither how often an image is scanned nor the history keys.
Implementation
- Environment identifier
- Purpose
- Give the unit that ties scan history together (the environment) an identifier that stays stable across reboots, container recreation, and migrations
- Implementation
- A configuration-supplied
environment.nameis the environment identifier- Runtime-derived values such as hostnames, Docker daemon IDs, and machine IDs are not used: they may be unreadable from inside a container or change on reinstallation, and none of them stay stable across reboots and container recreation (confirmed during the image identity model work)
environment.nameis validated as a DNS-1123 label (lowercase alphanumerics and hyphens, 1-63 bytes, starting and ending with an alphanumeric)- An empty string is the unnamed default environment, a distinct, valid state rather than a failed validation
- Uppercase, dots, underscores, whitespace, and control characters are all rejected rather than normalized
- Why no normalization: silently folding
"Prod"into"prod"would mean the tool decides on its own whether two configured names should share history, and that decision is left to whoever can fix the config value
- Kind (currently fixed at
docker) is not a configuration item- A config key for it could only ever record a value that might drift from the Runtime Adapter actually wired up, so the composition root (
main.go) sets it mechanically from whichever adapter it assembled
- A config key for it could only ever record a value that might drift from the Runtime Adapter actually wired up, so the composition root (
- A configuration-supplied
- Purpose
- Common model for workloads and containers
- Purpose
- Represent the container-to-service mapping that answers "which services does this image's CVE affect?" in a shared shape that does not depend on Docker-specific vocabulary (labels and so on)
- Implementation
- A Compose-launched container's Workload is resolved only when both
com.docker.compose.project(project) andcom.docker.compose.service(service) labels are present and valid- When either is missing, or a value is invalid (empty, over 253 bytes, or containing control characters), the whole thing stays
unknownrather than being treated as "partially known" - This follows the same rule the image identity model used to ban an intermediate state where canonical is false but ContentID is non-empty
- When either is missing, or a value is invalid (empty, over 253 bytes, or containing control characters), the whole thing stays
- A container's display name (
Container.Name) is never promoted into a Workload- Unlike a Compose service, it is a label an operator assigns per container at will, so it is kept as its own observed fact on
Container, separate from Workload
- Unlike a Compose service, it is a label an operator assigns per container at will, so it is kept as its own observed fact on
- Docker-specific information — container IDs, raw non-Compose labels, and the
Namesarray itself — stays inside the Docker adapter; onlyContainer{Name, Workload, Image}crosses into the common model
- A Compose-launched container's Workload is resolved only when both
- Purpose
- Adapter boundary: the primary observation moves from image to container
- Purpose
- The rework required to build this development's "containers and workloads in the webhook" feature
- The old Docker adapter, after enumerating containers, summarized them down to "the list of running images" before handing anything to the shared code, discarding the container information at the boundary
- That was fine as long as no feature used it; now that the shared code that builds notifications needs containers and workloads, the hand-off was changed to pass the pre-summary, container-level entries
- The rework required to build this development's "containers and workloads in the webhook" feature
- Implementation
- The Docker adapter's
RunningImageswas replaced byRunningContainers- Container-level observation (which container is running what) is now the primary data, and the set of distinct images to scan (
inventory.DistinctImages) is derived from it
- Container-level observation (which container is running what) is now the primary data, and the set of distinct images to scan (
- No additional Docker API call is made
- Workload and the display name are both built from the
Names/Labelsfields already present in the same/containers/jsonresponse
- Workload and the display name are both built from the
- The Docker adapter's
- Purpose
- Incorporating scope into state: the key space is unchanged
- Purpose
- Let the state file describe which environment its history belongs to, while guaranteeing that no operation — naming, renaming, un-naming, upgrading, or downgrading — triggers re-notification or history loss (the core promise of "never re-announce what is already known" stays intact)
- Implementation
- Environment was not added to the Findings/EOSL keys at all, only kept as a self-descriptive additional field,
State.Environment(environmenton disk)- At design time, whether the image identity model's approach (unchanged keys, only added fields) would still work was an open question, since a scope key touches the history subject itself rather than sitting alongside it; in the implementation, the same approach applied directly
Computeand the diff rules never read the field
- The reason is backward compatibility with older binaries
- Downgrading simply ignores the
environmentfield, so it never triggers a re-notification - Because naming, renaming, and un-naming an environment never change any history key, an existing finding's
FirstSeenis preserved through all of them, which structurally guarantees zero re-notifications from any of the three
- Downgrading simply ignores the
- The unnamed default environment (
environment.nameunset) omits theEnvironmentfield entirely (omitemptyplus a nil pointer)- This keeps an unconfigured user's state file byte-for-byte identical to the pre-Environment format
- A single state file still holds a single environment
- Consolidating multiple environments' history into one state file is out of scope for this phase and remains future work
- If a future phase changes the key space itself to merge multiple environments into one state file, the "just add a field" approach used here will not be enough — an explicit format migration, plus a policy for behaving safely on downgrade at switchover, will need to be designed separately
- Environment was not added to the Findings/EOSL keys at all, only kept as a self-descriptive additional field,
- Purpose
- Notifications: Slack stays a summary, the webhook carries everything
- Purpose
- Deliver the new environment and workload information through each outlet appropriately — to people (Slack) without hurting the summary's density, and to machines (the webhook) in full
- Leave the output of users who changed nothing unchanged
- Implementation
- The Slack product header gets the environment name inserted once, right at the start of the message, only when one is configured (
🛡️ *KestreLynx* [prod] — ...)- The unnamed default renders exactly the pre-existing text
- Renderers with no header line of their own (the thread report) are unaffected
- Workload and container information is not sent to Slack at all — only the generic webhook carries it
- This extends the existing "Slack summarizes, the webhook carries everything" policy
- The webhook payload was extended additively
- A top-level
environment {kind, name}was added (kindis always present;nameis omitted only for the unnamed default), plus acontainersarray (name plus workload) on every image entry - An unknown workload is still written out explicitly as
"kind": "unknown"rather than omitted - Why explicit: a receiver can tell "this payload doesn't carry workloads at all" (an older version) apart from "workload association could not be determined" (this cycle)
- No existing field was changed
- A top-level
- The Slack product header gets the environment name inserted once, right at the start of the message, only when one is configured (
- Purpose
Constraints
- Discovery remains read-only.
- Existing single-host Docker users must be able to keep the same basic setup and notification behavior without additional configuration.
- If an identifier is unavailable or a mapping is unknown, represent that state instead of guessing.
- Runtime-specific identifiers must not leak into the shared analysis model.
- The semantics of the image content identifier (the image config digest) do not change in this phase
- Environments and workloads are added as layers around it
Update history
August 29, 2026
- Implemented the environment and workload model
- The new
inventorypackage defines the common vocabulary of Environment/Workload/Container/RunningImage, and the Docker adapter's observation unit switched fromRunningImagestoRunningContainers - An optional
environment.name(DNS-1123 label) was added to configuration, and Environment and container information is carried consistently throughanalyze.Build, state, and Slack/webhook notifications
- The new
- State keeps its existing keys and version;
Environmentis recorded only as a self-descriptive additional field- A copy of the production state file was verified to load without conversion, and both Slack output and the state file's bytes were confirmed unchanged for the unnamed default environment, in tests and against that copy
- Naming, renaming, and un-naming the environment were all confirmed to produce zero re-notifications
- Consolidating multiple environments' history into one state file was not attempted
- A single state file still holds a single environment, left as future work
August 25, 2026
- Organized the implementation scope of the environment and workload model
This article is part of the KestreLynx development log. Learn more about KestreLynx