Capability manifest

A machine-readable description of every function's declared authorities, attached metadata, and boundary crossings, emitted directly by the compiler. Designed as an audit artefact for the EU Cyber Resilience Act era.

What CRA asks for, in plain terms

The EU Cyber Resilience Act, in full application from December 2027, requires manufacturers of "products with digital elements" to demonstrate security by design and by default, supply a machine-readable SBOM, document the product's security properties, and maintain that documentation across the product lifecycle. The expectation is not "your code is secure": it is "you can show, with evidence, what your code is allowed to do."

That last sentence is the hard one. Existing SBOM formats (SPDX, CycloneDX) describe which components are in a product. They do not describe what each component is authorised to do. The information simply does not exist in the source, no mainstream language carries it in the type system.

Capa does. The capability manifest is the export of that information into a stable, schema-versioned JSON document.

A concrete example

Take a small Capa program with a known security history:

@security(
    cve: "CVE-2024-99999",
    severity: "high",
    fixed_in: "0.2.0",
    description: "previous implementation was vulnerable to a timing attack"
)
fun verify_token(token: String, expected: String) -> Bool
    return token == expected

fun fetch_user(net: Net, id: String) -> Result<String, IoError>
    return net.get("https://api.example.com/users/${id}")

fun token_length(token: String) -> Int
    return token.length()

Run it through the compiler with the new flag:

$ python -m capa --manifest verify.capa

You get, on stdout:

{
  "capa_version": "0.2.0",
  "schema_version": 1,
  "functions": [
    {
      "name": "verify_token",
      "declared_capabilities": [],
      "has_unsafe": false,
      "attributes": [
        {
          "name": "security",
          "args": {
            "cve": "CVE-2024-99999",
            "severity": "high",
            "fixed_in": "0.2.0",
            "description": "previous implementation was vulnerable..."
          }
        }
      ]
    },
    {
      "name": "fetch_user",
      "declared_capabilities": ["Net"],
      "has_unsafe": false,
      "attributes": []
    },
    {
      "name": "token_length",
      "declared_capabilities": [],
      "has_unsafe": false,
      "attributes": []
    }
  ],
  "summary": {
    "total_functions": 3,
    "functions_with_capabilities": 1,
    "functions_with_attributes": 1,
    "functions_crossing_unsafe": 0
  }
}

An auditor reading this knows, without running the program: verify_token is pure (no capabilities) and references a fixed CVE; fetch_user can touch the network; token_length is pure. There is no other way for any of these to leak data or contact a server, the type system already rejected such a program at compile time.

Call-site visibility

Each function record also carries a calls[] array listing every function and method call inside its body, with the line:col of the call site and a stringified form of the argument expressions. This is where attenuation becomes visible at the audit level: the manifest does not just say "this function holds Net", it shows the actual restrict_to("api.example.com") call before the cap is handed onward.

{
  "name": "main",
  "declared_capabilities": ["Net", "Stdio"],
  "calls": [
    {
      "kind": "method",
      "callee": "net.restrict_to",
      "pos": "5:15",
      "args": ["\"api.example.com\""]
    },
    {
      "kind": "fn",
      "callee": "fetch_user",
      "pos": "6:18",
      "args": ["api", "\"42\""]
    }
  ]
}

The auditor can see, without opening the source, that the unrestricted net never reaches fetch_user: it is narrowed to api.example.com on the line before. The CycloneDX SBOM mirrors this in dependencies[]: main now has an explicit edge to fetch_user in addition to the user-capability components it declared.

Per-call data-flow

The same call record carries a parallel args_flow array. Where an argument names a binding produced by a chain of .restrict_to* calls, the corresponding entry records the bound name and the restrictions applied to it, in source order. For arguments that are literals or unrestricted parameters, the entry is null.

{
  "callee": "fetch_user",
  "args": ["narrower", "\"42\""],
  "args_flow": [
    {
      "name": "narrower",
      "attenuations": [
        { "method": "restrict_to",
          "args": ["\"api.example.com\""] },
        { "method": "restrict_to",
          "args": ["\"v2.api.example.com\""] }
      ]
    },
    null
  ]
}

The auditor sees the effective restriction surface the callee was given, not just the variable name. Source: a chain of let api = net.restrict_to("api.example.com") then let narrower = api.restrict_to("v2.api.example.com") then fetch_user(narrower, "42"). The narrowing history is reconstructed from the AST and surfaced in the static artefact.

v1 tracking is intentionally narrow: only ``let``-bindings whose right-hand side is a chain of ``.restrict_to*`` calls are recorded, and the walk is not lexical-scope-aware. Inter-procedural propagation (resolving mailer.send to the specific impl) is still out of scope and will arrive in a later pass.

How the manifest maps to CRA requirements

A non-exhaustive correspondence between fields the manifest emits and obligations the Act imposes. The mapping is not magic, it is just that capability-based security happens to align with what CRA was reaching for.

CRA obligation Manifest field that documents it
Security by design and by default: products operate with the minimum of privileges necessary. functions[].declared_capabilities, the exact set of authorities each function holds. A function that does not list Net cannot reach the network.
Documentation of security properties of the product. functions[].attributes[@security], CVE references, severity, fix versions, free-form description.
Vulnerability handling and lifecycle support. functions[].attributes[@deprecated], explicit deprecation markers with reason, since-version, and replacement function.
Audit trail throughout the product lifecycle. functions[].attributes[@audited], date, auditor, scope, free-form notes.
Identifying components where standard guarantees do not hold. functions[].has_unsafe, explicit flag set whenever a function declares Unsafe as a parameter. The compiler also surfaces it in summary.functions_crossing_unsafe.
SBOM enrichment: capabilities of each component. user_defined_capabilities, high-level capability declarations and their implementors. A library that exposes SendEmail declares its surface; a CI gate can verify that no caller obtains more.

The v1 attribute catalogue

Three attributes are recognised. The analyzer rejects unknown names, unknown keys, and duplicates, the schema is fixed so downstream consumers can rely on it.

@security

Link a function to a known security history. Keys: cve, cwe, severity, fixed_in, description.

@security(
    cve: "CVE-2024-12345",
    severity: "high",
    fixed_in: "0.2.0"
)

@deprecated

Mark an API as superseded. Keys: reason, since, use, removed_in.

@deprecated(
    reason: "timing attack",
    since: "0.2.0",
    use: "verify_token"
)

@audited

Record a manual security audit. Keys: date, by, scope, notes.

@audited(
    date: "2026-05-11",
    by: "Nelson Duarte",
    scope: "full"
)

How to use it in practice

Three natural integration points:

  1. CI gate. Run python -m capa --manifest src/*.capa in CI, diff the output against a baseline, and fail the build if a function unexpectedly gains a capability or crosses the Unsafe boundary. The capability surface of a library becomes a tracked artefact.
  2. Audit evidence. Generate the manifest at release time and ship it alongside the binary. An auditor can verify the claims without re-running the compiler.
  3. SBOM enrichment via CycloneDX 1.5. Capa also emits the manifest wrapped in a valid CycloneDX 1.5 SBOM (python -m capa --cyclonedx file.capa). The capability metadata is embedded as standard properties[] entries under the capa:* namespace, so Dependency-Track, OSV-Scanner, syft, and any other CycloneDX-aware tool can ingest the document and surface the information without having to know anything about Capa. See the section below.

The manifest is a stable contract. Versioned via schema_version; consumers should refuse to read manifests with a schema version they do not recognise. v1 is the current version. Breaking changes will bump it.

Human-readable counterpart: --doc

The manifest is for machines (CI gates, SBOM tools, audit pipelines). The same source carries enough information to produce a human-readable artefact too. python -m capa --doc file.capa emits a self-contained HTML page documenting every function, type, and user-defined capability, using:

The output is one HTML file with inline CSS, no external resources, no JavaScript. Ship it next to the manifest as the auditor's reading copy.

/// Verify a token against an expected value.
/// 
/// Caveat: this is a naive equality comparison; a real implementation
/// would use a constant-time compare to avoid timing attacks.
@security(
    cwe: "CWE-208",
    severity: "medium",
    description: "naive comparison; replace with constant-time before production"
)
fun verify_token(token: String, expected: String) -> Bool
    return token == expected

The rendered HTML carries the doc text as paragraphs, the CWE / severity / description as a `@security` row, and the function's pure-no-caps fact as the absence of any capability badge. A reviewer can read it without seeing the source file.

CycloneDX 1.5 output

The --manifest flag emits Capa's native JSON shape, compact, opinionated, and easy to consume from a Capa-aware tool. For interoperability with the broader supply-chain tooling ecosystem, Capa also emits a CycloneDX 1.5 SBOM with the same information embedded:

$ python -m capa --cyclonedx verify.capa

The resulting document is a valid CycloneDX 1.5 BOM and validates against the published schema. Each function and each user-defined capability becomes a library sub-component with a deterministic bom-ref. Per-function metadata (declared capabilities, attributes, Unsafe crossings, position) is flattened into the standard properties[] array under the capa:* namespace.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "serialNumber": "urn:uuid:2687e40d-...",
  "version": 1,
  "metadata": {
    "timestamp": "2026-05-12T00:00:00Z",
    "tools": { "components": [{ "name": "capa", "version": "0.2.0" }] },
    "component": { "name": "verify.capa", "type": "application" }
  },
  "components": [
    {
      "bom-ref": "capa:fn:verify.capa:verify_token",
      "type": "library",
      "name": "verify_token",
      "properties": [
        { "name": "capa:kind",                       "value": "function" },
        { "name": "capa:has_unsafe",                 "value": "false" },
        { "name": "capa:declared_capability",        "value": "Net" },
        { "name": "capa:attribute:security:cve",     "value": "CVE-2024-99999" },
        { "name": "capa:attribute:security:severity","value": "high" }
      ]
    }
  ],
  "dependencies": [
    { "ref": "capa:program:verify.capa", "dependsOn": ["capa:fn:verify.capa:verify_token"] }
  ]
}

What you get for free with CycloneDX

A consumer that is Capa-aware can read the capa:* properties to get the same information the native --manifest flag would emit. The two outputs carry the same content; the CycloneDX form trades a slightly more verbose wrapper for ecosystem fit.

What the manifest does not tell you

See it for yourself