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:
- CI gate. Run
python -m capa --manifest src/*.capain CI, diff the output against a baseline, and fail the build if a function unexpectedly gains a capability or crosses theUnsafeboundary. The capability surface of a library becomes a tracked artefact. - 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.
- 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 standardproperties[]entries under thecapa:*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:
- Doc comments (
///line and/** */block) attached to declarations. A small subset of markdown (paragraphs and inlinecodespans) is rendered; HTML special characters are always escaped. - Function signatures with capability parameters highlighted, and a badge per declared capability (with a distinct colour for
Unsafe). - Attributes (
@security,@deprecated,@audited) flattened to keyword-value rows. - Call list per function (collapsed by default), with the line:col and stringified argument expressions of every call site.
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
- Validation by any CycloneDX schema validator.
- Ingestion by Dependency-Track, OSV-Scanner, syft, sbom-utility, and other mainstream SBOM tooling. They will see the components and dependency graph without knowing anything Capa-specific.
- Deterministic
serialNumberderived from the filename (UUIDv5), so re-running the command on the same file produces an identical SBOM, friendly to diffing across releases. - Dependency edges from each function to the user-defined capabilities it depends on, so CycloneDX-aware tooling builds a usable graph.
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
- Whether a capability holder behaves well. A function that legitimately receives
Netcan do whateverNetallows, including misuse. The manifest documents the permission surface; it does not predict behaviour. - Anything beyond an
Unsafeboundary. Once a value crosses into Python interop, the discipline ends. The manifest marks the crossing (has_unsafe: true); content beyond it must be audited separately. - Dependencies you did not write. v1 only describes the program you compile. Aggregating manifests across the dependency graph is on the roadmap, it requires a module system first.
- Full data-flow tracking. The manifest now records every call site with its argument expressions, including
restrict_to(...)calls visible directly in the source, so an auditor reading the manifest can see thatmaincallsnet.restrict_to("api.example.com")before handing the cap tofetch_user. Full flow tracking, propagating restrictions through let-bindings and into method-call resolution, is a deeper analyser change still open for v0.4.