SBOB specification — v0.0.3
- Document
- Software Bill of Behavior
- Version
- 0.0.3
- Stage
- Proposed
- Status
- First implementation is being tested, Feedback welcome
- Where to report comments
- Typos: https://github.com/billofbehavior/fusioncore-web
Contents
Abstract
A Software Bill of Behavior (SBOB) is a signed, declarative document that states what a piece of software is intended to do at runtime — specifically, which Linux capabilities it requires, which files it expects to open, which child processes it spawns, which network destinations it accepts/establishes.
This document specifies version 0.0.3 of the SBOB document format. It does not specify in how many seperate files such a spec would be supplied, only the definitions of the fields.
1. Introduction
1.1 What an SBOB is for
The SBOB expresses, in machine-readable and human-readable form, the prescriptive intent of the software author: the set of behaviors the software is designed to exhibit.
The quality of an SBOB can be judged by:
- its ability to distinguish malicious from intentional behavior
- its transferability (portability) across linux-based systems
- its compatibility with standard gitOps tooling
- its usability by humans (time to first detection)
1.1.1 Kernel vs User-Space: Universal Applicability
The SBOB is designed to make runtime security more achievable, not to replace existing approaches such as the Linux Security Modules.
To serve all application languages, an SBOB is based on the lowest common denominator: the Linux Kernel and for high-compatibility, we will rely on the ABI or other long-term-stable elements.
1.2 Relationship to existing standards
| Other artifact | What it covers | Relationship to SBOB |
|---|---|---|
| SBOM (CycloneDX, SPDX) | static composition (packages, versions, licences) | complementary — solves the question of ingredients |
| RBOM (in-toto,rapidford) | execution-aware refinement of SBOM | complementary — solves the question of reachability |
| VEX | Vendor declared exploitability of a CVE | complementary — solves the question of CVE relevance |
kubescape ApplicationProfile | runtime-observed behavior captured by an in-cluster operator | Raw ingredient, this document shows how to abstract it into an SBOB |
| Seccomp / AppArmor profile | kernel-enforced syscall and resource policy | adjacent — an SBoB MAY transpile into a seccomp profile |
1.3 Conformance language
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals.
Reference implementation
This specification is in continuous feedback interaction with its reference implementation in CNCF Kubescape . We use two markers:
- Not all features are fully implemented / merged to
mainin CNCF Kubescape yet. Those are marked unmerged. - Some parts are not yet generalized from the reference implementation to a formal specification. Those are marked de-facto.
2. Conformance
A conformant SBOB document is a (set of) YAML document(s) that:
- Has the envelope described in §4.1, with
apiVersion,kind,metadata, andspecpopulated. - Contains at least one entry under spec.containers[].
- Populates each entry with at least name and imageID, plus any of the structural sections (§4.3 through §4.8) that apply.
- If any field uses a wildcard or pattern, that pattern conforms to §5.
- is of kind spdx.softwarecomposition.kubescape.io/v1beta1
- Distinguishes absent fields (NULL) from explicit-empty fields (NONE) per §5.4 in its YAML form, even when the underlying language binding collapses the two.
A conformant verifier:
- Matches the SBOB name using a label kubescape.io/user-defined-profile or user-defined-network
- Implements every match step in §6 against every section of §4 that the document populates.
- Treats absent fields (NULL — non-deterministic, implementation-defined posture) and explicit empty-collection fields (NONE — declared zero-activity, hard violation on first observation) differently (§5.4).
- Reports its NULL-handling posture (deny / log-only / learn-mode) at startup so operators know what their absent-field workloads will encounter.
- Emits drift events keyed by the canonical action verbs in Appendix B, carrying the engine-specific rule ID alongside the verb.
3. Document structure
An SBOB document is a single YAML stream containing one Kubernetes-style resource.
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
metadata:
name: <name-unique-to-environment> #mandatory
spec:
architectures: [...]
containers:
- type: init| null
- name: <container-name> #mandatory
imageID: <oci-image-with-digest>
capabilities: [...]
endpoints: [...]
execs: [...]
opens: [...]
egress: [...]
policyBinding: { ... }
4. Schema reference
4.1 Envelope
| Field | Type | Required | Description |
|---|---|---|---|
| apiVersion | string | required | MUST be "spdx.softwarecomposition.kubescape.io/v1beta1" for v0.0.3. Future SBOB versions MAY change this. |
| kind | string | required | MUST be "ApplicationProfile" or "NetworkNeighborhood" . This will change in summer 2026 |
| metadata.name | string | required | A short, stable name for the workload. RECOMMENDED: the OCI image’s repository:tag short form. |
| spec.architectures | list of strings | optional | Linux architecture tokens for which this SBOB applies (e.g. amd64, arm64). |
| spec.containers[] | list of objects | required | At least one entry. Each entry conforms to §4.2. |
4.2 Container entry
The container lifecycle phase (regular / init / ephemeral) is currently not implemented
as a field on the entry. Only,
Kubernetes PodSpec convention
—
is supported:
- spec.containers[] — regular workload containers
- spec.initContainers[] — init containers (run to completion before the regular containers start)
- spec.ephemeralContainers[] — debug / sidecar ephemeral containers (added post-creation)
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | A unique-within-document identifier for the container. |
| imageID | string | required | The OCI image reference, ideally pinned |
| capabilities | list of strings | optional | See §4.3. |
| endpoints | list of objects | optional | See §4.4. |
| execs | list of objects | optional | See §4.5. |
| opens | list of objects | optional | See §4.6. |
| ingress | list of objects | optional | See §4.7. highly recommended |
| egress | list of objects | optional | See §4.7. highly recommended |
| policyBinding | map | optional | See §4.8. |
4.3 capabilities
A list of Linux capabilities that the workload is permitted to hold. Each
entry MUST be a token from the kernel’s
capabilities(7)
manual page, in upper-case (e.g. CAP_NET_BIND_SERVICE).
capabilities: [CAP_SETGID, CAP_SETPCAP, CAP_SETUID]
A verifier MUST emit a violation if the live process holds any capability
not listed. The absence of the field carries a different meaning from
explicit empty (capabilities: []) — see §5.4.
4.4 endpoints
A list of HTTP-level endpoints the workload is expected to expose (direction: inbound) or call (direction: outbound).
| Field | Type | Description | |
|---|---|---|---|
| endpoint | string | Pattern of the form :<port>/<path>. Each component supports wildcards per §5. | |
| methods | list of strings | HTTP verbs (GET, POST, …) the endpoint accepts or sends. | |
| headers | map or null | Map of <header-name>: <expected-value-pattern>. | |
| direction | enum | inbound or outbound. |
endpoints:
- endpoint: ":0/api/data" # any port, exact path
methods: [GET, POST]
headers: null # explicit, no extra headers expected
direction: outbound
The desired degree of specificity in methods and headers is under debate
4.5 execs
A list of processes that the workload is permitted to spawn (via execve).
| Field | Type | Required | Description |
|---|---|---|---|
| path | string | required | Absolute filesystem path of the executable. Wildcards per §5. |
| args | list of strings | optional | Argument vector. Each entry is either a literal token or one of the exec-arg wildcard tokens defined in §5: ⋯ (DynamicIdentifier) matches exactly one argument position in a path, ⋯⋯ (ExecArgsWildcard) matches zero-or-more consecutive arguments. * is not an argument wildcard — it matches literally inside args. |
execs:
- path: /usr/sbin/apache2 # exact installation path
args: [/usr/sbin/apache2, '⋯⋯'] # anchored argv[0] then any tail
- path: /⋯/apache2 # relative installation path
args: [/⋯/apache2, '⋯⋯']
4.6 opens
A list of file paths the workload is permitted to open, with the open-flag combinations under which it is permitted to open them.
| Field | Type | Required | Description |
|---|---|---|---|
| path | string | required | Filesystem path, absolute. Wildcards per §5: ⋯ (U+22EF, DynamicIdentifier) matches exactly one segment; * (WildcardIdentifier) matches zero-or-more segments mid-path and one-or-more segments when trailing. |
| flags | list of strings | required | Subset of O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, O_CLOEXEC, O_NONBLOCK, O_DIRECTORY, O_NOFOLLOW, O_PATH. See Kernel documentation |
opens:
- flags: [O_RDONLY, O_WRONLY, O_CREAT]
path: /var/log/apache2/* # any descendant (1+ segments under, never the bare dir)
- flags: [O_RDONLY]
path: /etc/resolv.conf # exact
- flags: [O_RDONLY, O_WRONLY]
path: /opt/*/vendor/app/node/* # mid-path * is 0+ segments (§5.1), trailing * is 1+ segments
- flags: [O_RDONLY, O_WRONLY]
path: /opt/⋯/vendor/app/node/* # mid-path ⋯ is 1 segments (§5.1), trailing * is 1+ segments
4.7 egress and ingress
Two parallel lists the workload is permitted to exchange traffic with:
- egress[] — destinations the workload is permitted to dial (it initiates the connection)
- ingress[] — peers the workload is permitted to accept connections from (the peer initiates)
Both lists carry entries of the same shape (table below) and are evaluated by symmetric verifier paths.
| Field | Type | Required | Description |
|---|---|---|---|
| identifier | string | optional | Free-form short name to refer to this entry in policyBinding or in cross-document references. |
| type | enum | optional | external (default) or internal. |
| ipAddresses | list of strings | optional | New in v0.0.2. Each entry is one of: an IPv4/IPv6 literal, a CIDR (10.0.0.0/8, 2001:db8::/32), or the * sentinel meaning “any IP” (sugar for 0.0.0.0/0+::/0). The verifier matches the live observed IP against each entry per §5.7; an observation that matches ANY entry passes. Empty list [] is NONE per §5.4 (no IP traffic intended). |
| dnsNames | list of strings | optional | Set of DNS names (FQDN form with trailing dot RECOMMENDED) the verifier accepts. Each entry MAY use the wildcard tokens defined in §5.8: leading *.<suffix> (RFC 4592, exactly one label), mid <a>.⋯.<b> (DynamicIdentifier, exactly one label), or trailing <prefix>.* (one or more labels). Strings without these tokens are byte-equality. Replaces the deprecated single dns field. |
| podSelector | label selector | optional | Cluster-internal traffic: matches pods bearing these labels. |
| namespaceSelector | label selector | optional | Cluster-internal traffic: matches pods in namespaces bearing these labels. |
| ports[] | list of objects | required | Each entry: name (<protocol>-<port> e.g. TCP-443), protocol (TCP/UDP/SCTP), port (uint16, nullable per §5.4). |
spec:
containers:
- name: payment-app
egress:
- identifier: stripe-api
type: external
ipAddresses: ["162.0.217.171/32"]
dnsNames: [*.api.stripe.com.]
ports:
- {name: TCP-443, protocol: TCP, port: 443}
- identifier: cluster-dns
type: internal
namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: kube-system}
podSelector:
matchLabels: {k8s-app: kube-dns}
ports:
- {name: UDP-53, protocol: UDP, port: 53}
ingress:
- identifier: load-balancer-health
type: internal
ipAddresses: ["10.244.0.0/16"] # TBD: should there be a K8S-SVC-CIDR etc placeholder
ports:
- {name: TCP-8080, protocol: TCP, port: 8080}
- identifier: prometheus-scrape
type: internal
namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: monitoring}
podSelector:
matchLabels: {app.kubernetes.io/name: prometheus}
ports:
- {name: TCP-9090, protocol: TCP, port: 9090}
4.8 policyBinding
A map keyed by a vendor-neutral action verb whose value is a fine-grained allow / deny clause for that action.
Key: engine-specific rule ID, currently a numerical value referencing the CEL rule in kubescape (e.g. R0001).
Value :
| Field | Type | Description |
|---|---|---|
| <rule-id>.allowedProcesses | list of strings | Process comm names that ARE permitted to perform the action. |
policyBinding:
syscall.unshare: # vendor-neutral verb (in kubescape `Rule1006`)
allowProcess: [runc] # only runc may unshare
Future: The action verb namespace shall use lower-snake-case strings, prefixed by category:exec.* (process spawning),file.* (filesystem activity),net.* (network),syscall.* (raw syscalls) WIP
5. Pattern and wildcard semantics
Relevant to abstract exact kernel events into a semantic and portable description of intent.
5.1 Path wildcards * and ⋯
Two wildcard tokens are defined for path components.
⋯ (U+22EF) —
exactly one segment. A single Unicode codepoint, NOT three ASCII
periods. Matches one full path segment (zero-or-more characters that
do not contain /). Spelt
distinctly to avoid colliding with accidential use of globs.
* (PathWildcard) — variable arity, position-dependent.
- When
*appears mid-path (i.e. there are more segments after it), it matches zero or more consecutive segments. - When
*appears trailing (last segment of the pattern), it matches one or more remaining segments — never zero.
| Pattern | Matches | Does not match |
|---|---|---|
/etc/⋯ | /etc/passwd, /etc/hosts | /etc/, /etc/ssh/sshd_config, /etc/ssl/certs/ca.pem |
/etc/* (trailing) | /etc/passwd, /etc/ssh/sshd_config | /etc, /etc/ |
/a/*/b (mid) | /a/b, /a/x/b, /a/x/y/b | /a/x/c |
/a/⋯/b | /a/x/b | /a/b, /a/x/y/b |
5.3 Exec-argument wildcards
The * and ⋯ tokens of §5.1 are path wildcards. An exec entry’s args
vector uses
⋯⋯(WildcardExecs) — zero or more arguments. Absorbs a run of consecutive argv entries, including none:[/bin/foo, '⋯⋯']matches/bin/foowith any tail (or none), whereas[/bin/foo]matches only the bare invocation with no arguments.
* is not an argument wildcard — inside args it is a literal asterisk.
5.4 Absent (NULL) vs explicit-empty (NONE)
TBD
5.6 Port wildcards
Ports appear in two contexts curretly
endpoints[].endpoint string — for http, use the integer 0 to express the intent of
ANY PORT.:0/api/dataNetworkNeighbor.ports[].port — TODO
5.7 IP address matching
Matches an observed IP against an ipAddresses[] list using these forms in order. Each entry is one of:
| Form | Example | Semantics |
|---|---|---|
| IPv4 / IPv6 literal | 162.0.217.171, 2001:db8::1 | Byte-equality on the parsed IP. The textual canonicalisation is the verifier’s responsibility (e.g. 2001:db8::1 and 2001:0db8:0000:0000:0000:0000:0000:0001 MUST compare equal). |
| CIDR | 10.0.0.0/8, 2001:db8::/32 | The verifier parses the entry with net.ParseCIDR (or equivalent) once and stores the *net.IPNet; matches via IPNet.Contains(observedIP). |
| * | * | 0.0.0.0/0 (RFC 4632, all IPv4) and ::/0 (RFC 4291, all IPv6). Matches any observed IP. |
5.8 DNS name matching
Matches an observed DNS name (from the workload’s
own getaddrinfo / res_query event see §8.2) against the
dnsNames[]
| Form | Position | Semantics | Example | Matches | Doesn’t |
|---|---|---|---|---|---|
| Literal | — | byte-equality after trailing-dot normalisation | api.stripe.com. | api.stripe.com., api.stripe.com | v1.api.stripe.com. |
*.<suffix> | leading | RFC 4592 — exactly one label before <suffix> | *.example.com. | api.example.com., webhook.example.com. | v1.api.example.com., example.com. (apex), .example.com. (empty label) |
<a>.⋯.<b> | mid | DynamicIdentifier — exactly one label between <a> and <b> | svc.⋯.kubernetes.io. | svc.kube-system.kubernetes.io. | svc.kubernetes.io., svc.a.b.kubernetes.io. |
<prefix>.* | trailing | WildcardIdentifier — one or more labels after <prefix> (never zero) | mycorp.com.* | mycorp.com.api., mycorp.com.api.v1. | mycorp.com. (apex match — zero rejected) |
Rationale for the token choices
- Leading
*= exactly one label is RFC 4592 - Mid-label
*is non-standard — bind/coredns reject it, cilium uses regex, Calico has its own glob form. v0.0.2 deliberately uses the project’s existing⋯token for the mid position, so the wire format never claims RFC 4592 compliance for a non-standard shape and producers’ intent is unambiguous. - Trailing
*= one-or-more matches the path semantic in §5.1 — a trailing*MUST consume at least one label, never zero, somycorp.com.*does not silently allow access to the bare apex.
Apex matching
*.example.com. does NOT match the bare apex example.com. — same
defensive rule as path-side trailing-*. Producers that intend the
apex MUST list example.com. as a separate dnsNames entry. Same
rule for <prefix>.* trailing form.
Empty / ** rejection
A dnsNames[i] containing ** (recursive wildcard) MUST be rejected
List semantics
The verifier matches each observed name against EVERY entry in
dnsNames; an observation that matches ANY entry passes. Entries are
unordered. A NONE list (dnsNames: []) per §5.4 means “no DNS traffic
intended” — any DNS observation is a hard violation.
6. Verifier algorithm
WIP
7. Examples
7.1 Minimal example
The smallest legal SBoB:
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ApplicationProfile
metadata:
name: payment-app
spec:
containers:
- name: payment-app
imageID: ghcr.io/billofbehavior/payment-app@sha256:f6c18b0...
7.2 Full example with wildcards
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ApplicationProfile
metadata:
name: payment-app
annotations:
sbob.io/spec-version: "0.0.3"
spec:
architectures: [amd64]
containers:
- name: payment-app
imageID: ghcr.io/billofbehavior/payment-app@sha256:f6c18b0...
capabilities: [CAP_SETGID, CAP_SETPCAP, CAP_SETUID]
endpoints:
- endpoint: ":0/api/data" # any port, exact path
methods: [GET, POST]
headers: null # explicit: no extra headers
direction: outbound
execs:
path: /usr/sbin/apache2 # intended exact path
- args: [/usr/sbin/apache2, '⋯⋯'] # zero-or-more trailing args
- path: /usr/lib/postgresql/⋯/bin/postgres # version number does NOT matter
args: ["postgres"]
- path: /usr/lib/postgresql/16/bin/initdb # version number DOES matter
args: ["initdb"]
opens:
- flags: [O_RDONLY, O_WRONLY, O_CREAT]
path: /var/log/apache2/* # any descendant (1+ segments under, never the bare dir)
- flags: [O_RDONLY]
path: /etc/resolv.conf # exact
- flags: [O_RDONLY, O_WRONLY]
path: /opt/*/vendor/app/node/* # mid * is 0+ segments, trailing * is 1+ segments
egress:
- identifier: stripe-api
type: external
ipAddresses: # v0.0.2 plural form, supersedes singular ipAddress
- "162.0.217.171" # IPv4 literal
- "2001:db8::/32" # IPv6 CIDR (§5.7)
dnsNames: # supersedes the deprecated single dns field
- api.stripe.com. # literal FQDN
- "*.api.stripe.com." # RFC 4592 leading-* — exactly one label (§5.8)
ports:
- {name: TCP-443, protocol: TCP, port: 443}
- identifier: cluster-dns
type: internal
namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: kube-system}
podSelector:
matchLabels: {k8s-app: kube-dns}
ports:
- {name: UDP-53, protocol: UDP, port: 53}
- identifier: kube-svc-resolver # demonstrates the mid-label ⋯ form
type: internal
dnsNames:
- "svc.⋯.cluster.local." # leaves NAMESPACE label open to user
ports:
- {name: UDP-53, protocol: UDP, port: 53}
policyBinding:
syscall.unshare: # canonical verb (kubescape R1006, see Appendix B)
allowProcess: [runc] # only runc may unshare
kernel.module_load: # kubescape R1002 / Falco "Linux Kernel Module Loaded"
allowProcess: []
7.3 Companion stack-profile example
WIP: extend the document above with CPU stack profile + abstraction handles, see
spec-stackprofile-v0.0.1
8. Security considerations
Spec maturity
This is v0.0.3, a pre-1.0 specification. Field names and semantics are stabilising but MAY still change between minor versions; every change is recorded in Appendix A, and conformance claims MUST cite a pinned version.
9. Open issues
- Generalisation of rules: names and verbs
- Notations for common network-masks such as
{pod_cidr}to be discoverable/replaceable by bobctl - Implementation and example of bundling and signing of bundles
- Support for pod-lifecycle
- Guidance on
useful syscalls
10. References
- RFC 2119, Key words for use in RFCs to Indicate Requirement Levels. https://www.rfc-editor.org/rfc/rfc2119
- RFC 8174, Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words. https://www.rfc-editor.org/rfc/rfc8174
capabilities(7), Linux manual page. https://man7.org/linux/man-pages/man7/capabilities.7.htmlopen(2), Linux manual page. https://man7.org/linux/man-pages/man2/open.2.html- RFC 6335, Internet Assigned Numbers Authority (IANA) Procedures for the Management of the Service Name and Transport Protocol Port Number Registry — note on port
0. https://www.rfc-editor.org/rfc/rfc6335 - kubescape
ApplicationProfileCRD, schema source. https://github.com/kubescape/storage - SBoB stack-profile extension, draft v0.0.3.
spec-stackprofile-v0.0.1
Appendix B. Vendor rule-name mapping
B.1 Process / exec verbs
B.2 File-access verbs
B.3 Syscall verbs
B.4 Capability verbs
B.5 Network verbs
B.6 Credential / kernel verbs
B.7 Cryptominer heuristics
B.8 Profile-integrity verbs
B.9 Producer guidance
Appendix A. Change log
| Date | Version | Note |
|---|---|---|
| 2026-05-04 | 0.0.1 | Initial draft. |
| 2026-05-16 | 0.0.3 | Spec ↔ code alignment pass. §4.1 kind spelling corrected to NetworkNeighborhood (American — matches CRD, tooling, CI). §4.2 container-type field removed; replaced with a description of the kubescape PodSpec-style three-list layout (spec.containers[] / spec.initContainers[] / spec.ephemeralContainers[]). §4.4 headers entry sharpened with an explicit TODO about the json.RawMessage + omitempty binding collapsing NULL/NONE — workaround (headersIntent enum or custom unmarshaller) deferred to v0.0.2. §4.5 execs entry gains a TODO about admission-time enforcement of path REQUIRED (current CRD tags it opt for binding reasons). §4.8 rewritten to match the in-code rulePolicies field name + engine-rule-ID key + boolean containerAllowed value; the canonical-action-verb registry from Appendix B is retained as a v0.0.2 goal rather than a current normative wire format. §5.6 split into two rules: endpoint-string :0 wildcard (unchanged), and a new explicit rule that NetworkNeighbor.ports[].port uses null-pointer = “any port” (the RFC 6335 numeric-0 sentinel does NOT apply at this field). §5.3 exec-argument wildcards defined: ⋯⋯ (ExecArgsWildcard) matches zero-or-more args, ⋯ (DynamicIdentifier) exactly one; * is opens/path-only and matches literally inside args. Removed a non-normative subsection that did not reflect the implementation. |
| 2026-05-10 | 0.0.2 | Network wildcards landed. §5.7 IP matching promoted from TODO to normative — ipAddresses []string (new plural) accepts IPv4/IPv6 literals, CIDRs (net.ParseCIDR + IPNet.Contains), and * sentinel for any. Singular ipAddress deprecated, kept for back-compat. §5.8 DNS matching: leading *.<suffix> (RFC 4592, exactly one label), mid <a>.⋯.<b> (DynamicIdentifier, exactly one), trailing <prefix>.* (one or more, never zero). ** recursive form rejected — reserved for v0.0.3. §4.7 retitled “egress and ingress” — both directions are first-class, share the same NetworkNeighbor shape, and consume the same matcher implementation; example extended with two ingress[] entries (CIDR-based LB-probe + selector-based prometheus-scrape). §6.2 algorithm step renamed “network neighbour check” with direction-aware routing (pktType=='OUTGOING'→egress[], pktType=='INCOMING'→ingress[]) and a new net.ingress_unexpected drift verb. §7.2 example demonstrates IPv6 CIDR + leading-* DNS + mid-⋯ Kubernetes-service-FQDN form. |