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.
The extension is the dispatch
Section titled “The extension is the dispatch”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"]
Door one: YAML is parsed by serde
Section titled “Door one: YAML is parsed by serde”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.
Door two: a code template is a program
Section titled “Door two: a code template is a program”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.
Why this matters when you write one
Section titled “Why this matters when you write one”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.
Related
Section titled “Related”- Why polyglot templates covers when the code door is worth its cost.
- Template trust model covers what a code template can do once cxg runs it.
- Write your first template builds a code template and runs it through the engine.

