Skip to content

Annotation header schema

Applies to templates in these 11 languages: c (.c), cpp (.cpp, .cc, .cxx), go (.go), java (.java), javascript (.js, .mjs), perl (.pl), php (.php), python (.py), ruby (.rb), rust (.rs), shell (.sh).

It does not apply to YAML templates. The YAML engine deserializes the document and never reads comments — see YAML schema.

cxg reads the first 50 lines of the file and looks for one line per field. Every field is matched with this regex, with the field name substituted for {}:

(?m)^[\s]*(?:#|//!?|\*)?[\s]*@{}[\s]*:[\s]*(.+?)[\s]*$

In practice that accepts a line of any of these shapes, with any indentation, and takes the first match in the header:

# @id: my-template
// @id: my-template
//! @id: my-template
* @id: my-template
@id: my-template

A /* on the same line as the annotation does not match: the pattern accepts #, //, //!, *, or no prefix at all, and nothing else. In a C-style block comment, every annotation therefore has to sit on a continuation line beginning with *.

Loaded by the pinned binary, from a file whose @id is on the opening /* line and whose other annotations are on * lines. @name was read; @id was not, so the id fell back to the filename:

Terminal window
$ cxg --disable-update-check --no-color template info block-comment
[INF] Auto-update checks disabled
╔════════════════════════════════════════════════════════════════╗
║ Template Information ║
╚════════════════════════════════════════════════════════════════╝
ID: block-comment
Name: Fixture whose header is a block comment
Language: JavaScript
Severity: High
Author: docs.bugb.io generator
Description: Puts one annotation on the /* line and the rest on * lines.
Tags: fixture, javascript
File: ./templates/block-comment.js
Size: 272 bytes

An annotation whose value is empty is treated as absent. A line past the 50-line window is not read at all.

Annotation Value Required
@id single value yes
@name single value yes
@author single value yes
@severity single value yes
@description single value yes
@version single value no
@tags comma-separated list yes
@cwe comma-separated list no
@cvss number no
@confidence integer no
@context_vars comma-separated list of tokens no
@vuln_class single value no
@hypothesis_tags comma-separated list no
@batch_group single value no
@auto_probe true, yes, 1 — anything else is false no

“Required” is what cxg template validate checks: a template missing one still loads and still runs, with the fallbacks below. Nothing refuses to run a template for a missing annotation.

Values in a comma-separated list are split on commas, trimmed, and lowercased. A single value is trimmed and kept as written.

A value for @cvss or @confidence that does not parse as a number is discarded, and the annotation is treated as absent. No warning is printed.

Written Parsed as
critical Critical
high High
medium Medium
low Low
info or informational Info
anything else Medium

Loaded by the pinned binary, from a template whose header says @severity: catastrophic:

Terminal window
$ cxg --disable-update-check --no-color template info schema-fixture-bogus-severity
[INF] Auto-update checks disabled
╔════════════════════════════════════════════════════════════════╗
║ Template Information ║
╚════════════════════════════════════════════════════════════════╝
ID: schema-fixture-bogus-severity
Name: Fixture with an unrecognised severity
Language: Python
Severity: Medium
Author: docs.bugb.io generator
Description: Declares a severity the parser does not recognise.
Tags: fixture, python
File: ./templates/bogus-severity.py
Size: 285 bytes

cxg fills every field it needs, so a template with no header at all still loads under a name derived from its filename. Loaded by the pinned binary, from a file with no annotations:

Terminal window
$ cxg --disable-update-check --no-color template info schema-fixture-no-annotations
[INF] Auto-update checks disabled
╔════════════════════════════════════════════════════════════════╗
║ Template Information ║
╚════════════════════════════════════════════════════════════════╝
ID: schema-fixture-no-annotations
Name: schema fixture no annotations
Language: Python
Severity: Medium
Author: Unknown
Description: python template: schema-fixture-no-annotations
Tags: python
File: ./templates/schema-fixture-no-annotations.py
Size: 57 bytes

The language tag is added to @tags whether or not the header declares tags. The fallbacks themselves, from src/engine/common.rs:

Show create_metadata, which fills every field
// Read file content for metadata parsing
let content = std::fs::read_to_string(path).unwrap_or_default();
// Parse metadata from comment headers
let parsed = parse_metadata_from_comments(&content);
// Check if metadata was found before moving fields
let has_metadata = parsed.has_metadata();
// Fallback: derive ID from filename
let fallback_id = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
// Use parsed values or fallbacks
let id = parsed.id.unwrap_or_else(|| fallback_id.clone());
let name = parsed
.name
.unwrap_or_else(|| fallback_id.replace(['-', '_'], " "));
let author_name = parsed.author.unwrap_or_else(|| "Unknown".to_string());
let severity = parsed
.severity
.map(|s| parse_severity_string(&s))
.unwrap_or(Severity::Medium);
let description = parsed
.description
.unwrap_or_else(|| format!("{} template: {}", language, fallback_id));
// Tags: use parsed tags, ensure language tag is always included
let mut tags = parsed.tags;
let lang_tag = language.to_string().to_lowercase();
if !tags.contains(&lang_tag) {
tags.push(lang_tag);
}
// If no tags were found at all, just use language tag
if tags.is_empty() {
tags.push(language.to_string().to_lowercase());
}
// Log if metadata was found
if has_metadata {
tracing::debug!(
"Parsed metadata from {}: id={}, tags={:?}",
path.display(),
id,
tags
);
}
TemplateMetadata {
id,
name,
author: crate::types::AuthorInfo {
name: author_name,
email: None,
github: None,
},
severity,
description,
cve_ids: Vec::new(),
cwe_ids: parsed.cwe,
cvss_score: parsed.cvss,
tags,
language,
file_path: path.to_path_buf(),
created: chrono::Utc::now(),
updated: chrono::Utc::now(),
version: parsed.version.unwrap_or_else(|| "1.0.0".to_string()),
confidence: parsed.confidence.or(Some(50)),
context_vars: parsed
.context_vars
.iter()
.map(|cv| {
let name = if cv.is_array {
format!("{}[]", cv.name)
} else {
cv.name.clone()
};
let qualifier = if cv.required { "required" } else { "optional" };
format!("{}:{}", name, qualifier)
})
.collect(),
vuln_class: parsed.vuln_class,
hypothesis_tags: parsed.hypothesis_tags,
batch_group: parsed.batch_group,
auto_probe: parsed.auto_probe,
}

With no @tags line, cxg scrapes tags out of the code with these patterns. Each is tried, and everything found is merged:

Looks like Pattern
Python/Ruby style - self.tags = […] or tags = […] (?:self\.)?tags\s*=\s*\[([^\]]+)\]
JavaScript/JSON style - tags: […] tags\s*:\s*\[([^\]]+)\]

A single context variable declaration parsed from @context_vars. Format in header: name:required or name[]:optional The [] suffix indicates the variable is a JSON array at runtime.

Token Meaning
name:required the template cannot operate without it
name:optional used when present
name[]:required the value is a JSON array at runtime
name no qualifier: optional

Qualifiers are case-insensitive, and required, req and r all mean required. Anything else means optional. Values reach the template as JSON in CERT_X_GEN_CONTEXT — see How cxg executes templates.

What cxg template validate says about a missing header

Section titled “What cxg template validate says about a missing header”
Terminal window
$ cxg --disable-update-check --no-color template validate templates/schema-fixture-no-annotations.py
[INF] Auto-update checks disabled
════════════════════════════════════════════════════════════════════════════════
CERT-X-GEN Template Validator
════════════════════════════════════════════════════════════════════════════════
Found 1 template(s) to validate
✓ schema-fixture-no-annotations.py
[warning] 3: common.missing_target_host: Template does not appear to use CERT_X_GEN_TARGET_HOST or target variables. Templates should be dynamic. Add: HOST = os.environ.get('CERT_X_GEN_TARGET_HOST') or similar
[warning] 1: common.missing_metadata: Template is missing metadata annotations. Add @field: annotations at the top of the file. Required: @id, @name, @author, @severity, @description, @tags
[warning] 1: enhanced.missing_network_code: Template does not appear to have network/socket code. Security templates typically need network connectivity. Suggestion: import socket
[warning] 1: enhanced.missing_error_handling: No error handling detected. Templates should handle errors gracefully to avoid crashes during scanning. Suggestion: try:
# code
except Exception as e:
pass
[info] 1: enhanced.no_entry_point: No main() function or __name__ guard found. Recommendation: def main():
pass
if __name__ == '__main__':
main()
[info] schema.no_findings_array: No 'findings' array detected. CERT-X-GEN expects output with a 'findings' array. Structure: {"findings": [{...}], "metadata": {...}}
[info] schema.missing_template_id: Required Finding field 'template_id' not found in template. Ensure findings include this field.
[info] schema.missing_template_name: Required Finding field 'template_name' not found in template. Ensure findings include this field.
[info] schema.missing_severity: Required Finding field 'severity' not found in template. Ensure findings include this field.
[info] schema.missing_host: Required Finding field 'host' not found in template. Ensure findings include this field.
[info] schema.missing_matched_at: Required Finding field 'matched_at' not found in template. Ensure findings include this field.
════════════════════════════════════════════════════════════════════════════════
Validation Summary
════════════════════════════════════════════════════════════════════════════════
Total Templates: 1
✓ Passed: 1
✗ Failed: 0
Success Rate: 100%