fusioncore.ai fusioncore.ai

← Bill of Behavior

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
  1. Abstract
  2. 1. Introduction
  3. 2. Conformance
  4. 3. Document structure
  5. 4. Schema reference
  6. 5. Pattern and wildcard semantics
  7. 6. Verifier algorithm
  8. 7. Examples
  9. 8. Security considerations
  10. 9. Open issues for v0.0.2
  11. 10. References
  12. Appendix B. Vendor rule-name mapping
  13. Appendix A. Change log

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 artifactWhat it coversRelationship to SBOB
SBOM (CycloneDX, SPDX)static composition (packages, versions, licences)complementary — solves the question of ingredients
RBOM (in-toto,rapidford)execution-aware refinement of SBOMcomplementary — solves the question of reachability
VEXVendor declared exploitability of a CVEcomplementary — solves the question of CVE relevance
kubescape ApplicationProfileruntime-observed behavior captured by an in-cluster operatorRaw ingredient, this document shows how to abstract it into an SBOB
Seccomp / AppArmor profilekernel-enforced syscall and resource policyadjacent — 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 main in 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:

  1. Has the envelope described in §4.1, with apiVersion, kind, metadata, and spec populated.
  2. Contains at least one entry under spec.containers[].
  3. Populates each entry with at least name and imageID, plus any of the structural sections (§4.3 through §4.8) that apply.
  4. If any field uses a wildcard or pattern, that pattern conforms to §5.
  5. is of kind spdx.softwarecomposition.kubescape.io/v1beta1
  6. 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:

  1. Matches the SBOB name using a label kubescape.io/user-defined-profile or user-defined-network
  2. Implements every match step in §6 against every section of §4 that the document populates.
  3. 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).
  4. Reports its NULL-handling posture (deny / log-only / learn-mode) at startup so operators know what their absent-field workloads will encounter.
  5. 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

FieldTypeRequiredDescription
apiVersionstringrequiredMUST be "spdx.softwarecomposition.kubescape.io/v1beta1" for v0.0.3. Future SBOB versions MAY change this.
kindstringrequiredMUST be "ApplicationProfile" or "NetworkNeighborhood" . This will change in summer 2026
metadata.namestringrequiredA short, stable name for the workload. RECOMMENDED: the OCI image’s repository:tag short form.
spec.architectureslist of stringsoptionalLinux architecture tokens for which this SBOB applies (e.g. amd64, arm64).
spec.containers[]list of objectsrequiredAt 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)
FieldTypeRequiredDescription
namestringrequiredA unique-within-document identifier for the container.
imageIDstringrequiredThe OCI image reference, ideally pinned
capabilitieslist of stringsoptionalSee §4.3.
endpointslist of objectsoptionalSee §4.4.
execslist of objectsoptionalSee §4.5.
openslist of objectsoptionalSee §4.6.
ingresslist of objectsoptionalSee §4.7. highly recommended
egresslist of objectsoptionalSee §4.7. highly recommended
policyBindingmapoptionalSee §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).

FieldTypeDescription
endpointstringPattern of the form :<port>/<path>. Each component supports wildcards per §5.
methodslist of stringsHTTP verbs (GET, POST, …) the endpoint accepts or sends.
headersmap or nullMap of <header-name>: <expected-value-pattern>.
directionenuminbound 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).

FieldTypeRequiredDescription
pathstringrequiredAbsolute filesystem path of the executable. Wildcards per §5.
argslist of stringsoptionalArgument 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.

FieldTypeRequiredDescription
pathstringrequiredFilesystem 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.
flagslist of stringsrequiredSubset 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.

FieldTypeRequiredDescription
identifierstringoptionalFree-form short name to refer to this entry in policyBinding or in cross-document references.
typeenumoptionalexternal (default) or internal.
ipAddresseslist of stringsoptionalNew 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).
dnsNameslist of stringsoptionalSet 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.
podSelectorlabel selectoroptionalCluster-internal traffic: matches pods bearing these labels.
namespaceSelectorlabel selectoroptionalCluster-internal traffic: matches pods in namespaces bearing these labels.
ports[]list of objectsrequiredEach 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 :

FieldTypeDescription
<rule-id>.allowedProcesseslist of stringsProcess 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.
PatternMatchesDoes 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/foo with 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/data

  • NetworkNeighbor.ports[].portTODO

5.7 IP address matching

Matches an observed IP against an ipAddresses[] list using these forms in order. Each entry is one of:

FormExampleSemantics
IPv4 / IPv6 literal162.0.217.171, 2001:db8::1Byte-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).
CIDR10.0.0.0/8, 2001:db8::/32The 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[]

FormPositionSemanticsExampleMatchesDoesn’t
Literalbyte-equality after trailing-dot normalisationapi.stripe.com.api.stripe.com., api.stripe.comv1.api.stripe.com.
*.<suffix>leadingRFC 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>midDynamicIdentifier — exactly one label between <a> and <b>svc.⋯.kubernetes.io.svc.kube-system.kubernetes.io.svc.kubernetes.io., svc.a.b.kubernetes.io.
<prefix>.*trailingWildcardIdentifier — 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, so mycorp.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

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

DateVersionNote
2026-05-040.0.1Initial draft.
2026-05-160.0.3Spec ↔ 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-100.0.2Network 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.
/spec-doc