fusioncore.ai fusioncore.ai

← Bill of Behavior

SBOB specification — v0.0.4

Document
Software Bill of Behavior
Version
0.0.4
Stage
Proposed
Contents
  1. Abstract
  2. 1. Introduction
  3. 1.4 Generalized rules: the portable set
  4. 2. Conformance
  5. 3. Document structure
  6. 4. Schema reference
  7. 5. Pattern and wildcard semantics
  8. 6. Verifier algorithm
  9. 7. Examples
  10. 8. Signatures
  11. 9. Security considerations
  12. 10. Open issues
  13. 11. References
  14. 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.

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
ContainerProfileruntime-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.

1.4 Generalized rules: the portable set

Tool 1 is the reference implementation. The Tool 2, Tool 3 and Tool 4 columns (purple) were compiled by an AI agent on 2026-09-20 from those projects' public rule files and documentation. They have not been verified against a running installation and may be wrong.

Cell legend: Tool 1 cells abbreviate the rule’s CEL predicate, prefixed by its event type; the verbatim expressions are in the shipped rule set (default-rules.yaml, chart tag kubescape-operator-1.41.0-duckling16). native = shipped; generated = emitted from the SBOB by tooling; deny-list = expressible only as an explicit deny of the observed value; N/A = no equivalent found in the indexed sources.

1.4.1 Rules that read the SBOB

Generalized ruleSBOB sectionTool 1Tool 2Tool 3Tool 4
Process executionexecs[].path (key process-execution)exec: !was_executednot proc.exepath in (sbob_execs)process[] {path, action: allow}deny-list: data.pathname!=<path>
Process argumentsexecs[].args (key process-arguments)exec: !was_executed_with_argsproc.cmdline regex <pattern>N/AN/A
File accessopens[].path (key file-access)open: !was_path_openednot fd.name pmatch (sbob_opens)file[] {filter, recursive} monitor listdeny-list: data.pathname!=<path>
Domain communicationegress[].dnsNames (key domain-communication)dns: !is_domain_in_egressN/Aaddress group per name, egress allowN/A
Egress trafficegress[] (key egress-traffic)network: OUTGOING && !in_egressoutbound and not fd.sip in (sbob_egress)egress[] {selector, ports, action: allow}deny-list: security_socket_connect
Ingress trafficingress[] (key ingress-traffic)network: HOST && !in_ingressinbound and not fd.cip in (sbob_ingress)ingress[] {selector, ports, action: allow}N/A

1.4.2 Signature rules; the SBOB allowlists who may perform them

These rules fire without an SBOB. The SBOB section for them is rulePolicies: per rule, the processes (processAllowed, by comm) or the whole container (containerAllowed) that are intended to perform the action. The engine implements the detection; the SBOB supplies the exemption.

Generalized ruleSBOB sectionTool 1Tool 2Tool 3Tool 4
Binary not in container imagerulePolicies.binary-not-in-imageexec: upperlayer && !was_executednative Drop and execute new binary in containernative zero-driftN/A
Execution from memoryrulePolicies.execution-from-memoryexec: exepath ~ memfdnative Fileless execution via memfd_createN/Anative TRC-105
Execution from shared memoryrulePolicies.execution-from-shared-memoryexec: exepath ~ /dev/shmnative Execution from /dev/shmgenerated: process[] {path: /dev/shm/*, action: deny}N/A
Sensitive file accessrulePolicies.sensitive-file-accessopen: path ~ /etc/shadownative Read sensitive file untrustedgenerated: file[] {filter: /etc/shadow, behavior: block_access}N/A
Link over sensitive filerulePolicies.link-over-sensitive-filesymlink/hardlink: oldPath ~ shadownative Create Symlink Over Sensitive Files, Create Hardlink Over Sensitive FilesN/AN/A
Service-account token accessrulePolicies.service-account-token-accessopen: path ~ sa tokenN/AN/Anative TRC-108
Procfs environment readrulePolicies.procfs-environment-readopen: path ~ /proc/*/environnative Read environment variable from /proc filesN/AN/A
Kubernetes API accessrulePolicies.kubernetes-api-accessexec/network: kubectl, API addrnative Contact K8S API Server From ContainerN/AN/A
Module loadrulePolicies.module-loadkmod: init_modulenative Linux Kernel Module Injection DetectedN/Anative TRC-1017
eBPF program loadrulePolicies.ebpf-program-loadbpf: cmd == 5native BPF Program Not ProfiledN/AN/A
Namespace changerulePolicies.namespace-changeunshare: pcomm != runcnative Change namespace privileges via unshareN/AN/A
Process tracingrulePolicies.process-tracingptrace: truenative PTRACE attached to process, PTRACE anti-debug attemptN/Anative TRC-103, TRC-102
Dynamic linker hookrulePolicies.dynamic-linker-hookexec/open: ld hookN/AN/Anative TRC-107
SSH connectionrulePolicies.ssh-connectionssh: dstPort not in [22]native Disallowed SSH Connection Non Standard Portpartial: suspicious process sshN/A
Cgroup release_agent writerulePolicies.cgroup-release-agent-writeN/Anative Detect release_agent File Container EscapesN/Anative TRC-1010
Stdio to socketrulePolicies.stdio-to-socketN/Anative Redirect STDOUT/STDIN to Network Connection in ContainerN/Anative TRC-101

1.4.3 rulePolicies in the SBOB

spec:
  rulePolicies:
    file-access:
      processAllowed: [logrotate]
    service-account-token-access:
      containerAllowed: true
    module-load:
      processAllowed: [modprobe]

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. Describes exactly one Pod with all ContainerTypes.
  3. Populates at least spec.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. 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 on the pod
  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 generalized rule names of §1.4, carrying the engine-specific rule ID alongside the name.

3. Document structure

An SBOB document is a single YAML stream containing one Kubernetes-style resource.

kind: ContainerProfile
metadata:
  name: <name-unique-to-environment>   # mandatory; the pod label binds to this value
  namespace: <namespace>
spec:
  architectures: [...]
  imageID: <oci-image-with-digest>
  capabilities:   [...]
  endpoints:      [...]
  execs:          [...]
  opens:          [...]
  ingress:        [...]
  egress:         [...]
  rulePolicies:   { ... }

One document describes one container. A pod with several containers carries one ContainerProfile per container, each bound by its own label value.

4. Schema reference

4.1 Envelope

FieldTypeRequiredDescription
apiVersionstringrequired
kindstringrequired
metadata.namestringrequiredA short, stable name for the container.
metadata.namespacestringrequiredNamespace of the workload the profile binds to.
metadata.annotationsmapoptional
spec.architectureslist of stringsoptionalLinux architecture tokens for which this SBOB applies (e.g. amd64, arm64).
spec.imageIDstringrequiredThe OCI image reference, ideally pinned by digest.
spec.*sectionsoptionalThe behavioural sections listed in §4.2.

4.2 Sections

All sections sit directly under spec. There is no per-container list; init and ephemeral containers get their own ContainerProfile documents.

FieldTypeRequiredDescription
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
rulePoliciesmapoptionalSee §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. A token containing ⋯ is split on / and ⋯ MUST equal a whole segment; a prefix that stops mid-segment (/a/b/elasticsearch⋯, --config=⋯) matches nothing and is rejected by bobctl validate.
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.

The two stanzas mirror the Kubernetes NetworkPolicy model as closely as the runtime view allows: the same two directions, the same podSelector / namespaceSelector / ipBlock peer vocabulary, and the same ports[] shape. An SBOB SHOULD be expressible as a NetworkPolicy and a NetworkPolicy SHOULD be expressible as the observed-intent half of an SBOB, so that a declared profile and an enforced policy can be diffed against one another. Where the two diverge, the divergence is stated: an SBOB additionally carries dnsNames (a query name the workload resolved, which NetworkPolicy cannot express) and records observed addresses rather than admission-time selectors.

FieldTypeRequiredDescription
identifierstringoptionalFree-form short name to refer to this entry in rulePolicies 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:
  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 rulePolicies

A map keyed by a generalized rule name (§1.4, kebab case) whose value says who is intended to perform that action. It is the SBOB’s allowlist for the signature rules of §1.4.2; the rules of §1.4.1 read their own sections and ignore it.

Key: the generalized rule name (namespace-change, module-load, …). The verifier maps it to its engine rule ID and carries both in the drift event.

Value:

FieldTypeDescription
processAllowedlist of stringsProcess comm names that ARE intended to perform the action.
containerAllowedbooltrue: any process in the container may perform the action. false is identical to omitting the entry.
rulePolicies:
  namespace-change:
    processAllowed: [runc]         # only runc may unshare
  file-access:
    processAllowed: [logrotate]

An entry for a rule the engine does not honour (§1.4.3) has no effect and MUST NOT be treated as a deny.

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

  • egress[].ports[].port / ingress[].ports[].port — an absent port (null) means ANY PORT; an omitted ports stanza means any port on that peer. A populated ports[] matches only its literal (protocol, port) entries. The RFC 6335 numeric-0 sentinel does NOT apply at this field.

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
Literal—byte-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:

kind: ContainerProfile
metadata:
  name: payment-app
  namespace: shop
  annotations: {kubescape.io/managed-by: User}
spec:
  imageID: ghcr.io/billofbehavior/payment-app@sha256:f6c18b0...

7.2 Full example with wildcards

kind: ContainerProfile
metadata:
  name: payment-app
  namespace: shop
  annotations:
  kubescape.io/managed-by: User
  sbob.io/spec-version: "0.0.4"
spec:
  architectures: [amd64]
  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}
  rulePolicies:
    namespace-change:            # §1.4.2 (kubescape R1006, Falco "Change namespace privileges via unshare")
      processAllowed: [runc]     # only runc may unshare
    module-load:                 # §1.4.2 (kubescape R1002, Falco "Linux Kernel Module Injection Detected", Tracee TRC-1017)
      containerAllowed: false    # same as omitting the entry; shown for clarity

8. Signatures

WIP

9. Security considerations

10. 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

11. References

Appendix A. Change log

DateVersionNote
2026-05-040.0.1Initial draft.
2026-09-200.0.4Document syntax. One document per container, flat spec (no containers[]), imageID at top level, metadata.namespace plus a provenance annotation, one binding label on the pod. §4.8 renamed rulePolicies, keyed by generalized rule name (§1.4), values processAllowed / containerAllowed. §4.5 whole-segment ⋯ rule stated. §1.4 generalized rules added. §5.6 port semantics stated. §6 reads ingress[] / egress[] from the same document.
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