Skip to content

How cxg executes templates

cxg reads a template through one of two entirely separate front doors, and the file extension chooses which. A .yaml file is parsed, in-process, into a typed structure cxg understands. A .py, .go, or .js file is not parsed at all. It is a program cxg runs, reading a header of comments for metadata and standard output for findings. Knowing which door your template goes through explains what can go wrong with it and where the trust boundary sits.

When cxg loads a directory, it looks at each file’s extension and hands the file to the first engine that claims it. Twelve extensions are recognised: yaml/yml, py, js, rs, c, cpp/cc/cxx, java, go, rb, pl, php, sh/bash. Anything else is skipped as a non-template file (src/template/engine.rs).

Each language has its own engine, and each engine answers one question: does this file belong to me? The YAML engine claims yaml and yml (src/engine/yaml/mod.rs); the Python engine claims py (src/engine/python/mod.rs), and so on for the other ten. There is no content sniffing: rename a Python template to .txt and cxg does not load it; rename it to .rb and cxg hands it to Ruby.

flowchart TD
    F["template file"] --> E{"extension?"}
    E -->|"yaml / yml"| Y["YAML engine"]
    E -->|"py js rs c cpp java<br/>go rb pl php sh"| C["language engine"]
    Y --> S["serde parses the file<br/>into a typed struct"]
    C --> H["comment header parsed<br/>for @id, @severity, …"]
    S --> M["matchers run in-process"]
    H --> R["file runs as a child process<br/>via its language runtime"]
    R --> J["findings read from stdout"]

A YAML template is data. cxg deserialises it with serde into a fixed structure of metadata, requests, and matchers, and every field is typed (src/engine/yaml/mod.rs). A matcher’s type is an enum with a closed set of variants, so a typo is a load error, not a silent no-op:

WARN cert_x_gen::template::engine: Failed to load template response-manipulation.yaml:
YAML parse error: http[3].matchers[0].type: unknown variant `dsl`,
expected one of `status`, `word`, `regex`, `binary`, `time`, `size`,
`hash`, `tls`, `dns`, `diff`, `custom`

That error is serde reporting a value outside the schema. The upside of a parsed template is that cxg can read it without running it: it can list, filter, and search YAML templates by their fields, and a YAML template can do nothing except what its schema allows: issue requests and match responses. It has no runtime of its own.

An eleven-language template is not parsed into a schema, because it is not data. cxg reads two things from it: a header of @-prefixed comments for metadata, and its standard output for findings.

The metadata parser scans only the first 50 lines of the file for @id, @name, @severity, @description, and the rest (src/engine/common.rs). This is the same header across all eleven code languages. The comment syntax differs, the field names do not, which is why the annotation block has to sit at the top of the file. Put it below line 50 and cxg does not see it.

Everything else is the program’s own affair. cxg resolves the language, launches the file through that language’s interpreter or compiled binary as a child process, and reads the JSON array it prints to stdout. Compiled languages, namely C, C++, Go, Java, and Rust, are built first and the build is cached; interpreted ones run directly. A YAML matcher and a Python template are answering the same question, but only the second one gets to compute the answer.

The language boundary is a process boundary

Section titled “The language boundary is a process boundary”

This is the fact that matters for reasoning about a code template: the boundary between cxg and the template is an operating-system process boundary, and nothing more. cxg spawns the interpreter; the interpreter runs your file with whatever privileges cxg has.

Two consequences follow, and they are the reason this page exists:

  • A missing runtime is a per-template failure, not a scan failure. If a template’s language is not on PATH, only that template fails. cxg logs a warning, records nothing, and moves on. A scan of eleven-language templates on a host that has only Python quietly runs a fraction of what you think it does. Why polyglot templates covers why that is dangerous.
  • There is no confinement at that boundary. A child process launched as you runs as you. cxg does not sandbox it, and no configuration key makes it. What a code template can reach is the subject of the template trust model, and you should read it before running a template you did not write.

Choosing YAML or code is choosing a door. YAML buys inspectability and a runtime that cannot exceed its schema, at the cost of never computing anything. Code buys arbitrary computation and multi-step logic, at the cost of a runtime dependency on every scanning host and a process that runs with your privileges. The engine does not blur these. The extension commits you to one.