Write your first template
You will write a Python template that confirms an exposed .git directory, run
it standalone, validate it, and then execute it through cxg against a target on
your own machine.
The detection is deliberately one a YAML template cannot express: the second
request it makes is chosen by parsing the first response, and it reports nothing
unless the whole chain holds. That is the difference between finding a .git
directory and proving one. See
Why polyglot templates for the reasoning.
Before you start
Section titled “Before you start”- cxg installed and on your
PATH. See Installation. - Python 3 and
git. - A working directory you can delete afterwards.
Everything below uses only the Python standard library, so the template runs
anywhere python3 does.
Build the target
Section titled “Build the target”Give yourself something with a real, exposed .git directory.
-
Create a repository with one commit and serve the directory over HTTP.
Terminal window mkdir cxg-first-templatecd cxg-first-templategit init -q .git -c user.email=you@example.com -c user.name=You commit -q --allow-empty -m "demo"python3 -m http.server 8000 --bind 127.0.0.1 -
In a second terminal, in the same directory, confirm
.git/HEADis reachable.Terminal window curl -s http://127.0.0.1:8000/.git/HEADref: refs/heads/masterIf your git defaults to
main, you will seerefs/heads/main. The template reads whichever it finds, which is the whole point.
The contract a template has to meet
Section titled “The contract a template has to meet”cxg runs a template as a child process and reads its standard output. Three things matter.
The annotation header. cxg parses @-prefixed comments at the top of the
file for the metadata it uses to list, filter, and search templates. @id,
@name, @severity, and @description are the ones you always want.
Environment variables carry the target. cxg sets these before running your template:
| Variable | Meaning |
|---|---|
CERT_X_GEN_TARGET_HOST |
Target host or IP |
CERT_X_GEN_TARGET_PORT |
Target port |
CERT_X_GEN_MODE |
engine when cxg is the caller |
CERT_X_GEN_CONTEXT |
JSON passed through from --context |
Findings go to stdout as a JSON array. An empty array means no finding, and that is the normal result. Anything you write to stderr is ignored by the parser, which makes stderr the right place for diagnostics.
You can print a scaffold for any supported language with
cxg template skeleton python. The skeleton is a class-based starting point that
imports requests; the template below is written from scratch against the
standard library instead, so it has no dependency to install.
Write the template
Section titled “Write the template”Create git-head-exposed.py in your working directory.
The header
Section titled “The header”#!/usr/bin/env python3## @id: exposed-git-head-verified# @name: Exposed .git directory (verified)# @author: Your Name# @severity: high# @description: Confirms an exposed .git directory by following HEAD to its ref and checking that the ref resolves to a real commit object ID.# @tags: exposure, information-disclosure, web, git# @cwe: CWE-527# @confidence: 95# @references: https://cwe.mitre.org/data/definitions/527.html@id is what you pass to --templates later, so make it stable. @severity is
one of critical, high, medium, low, info.
The detection
Section titled “The detection”Three steps, each gating the next.
import jsonimport osimport reimport sysimport urllib.errorimport urllib.request
TIMEOUT = 5
def get(url): """Fetch a URL, returning the body as text, or None on any failure.""" try: with urllib.request.urlopen(url, timeout=TIMEOUT) as response: if response.status != 200: return None return response.read(8192).decode("utf-8", errors="replace") except (urllib.error.URLError, OSError): return None
def scan(host, port): scheme = "https" if port == 443 else "http" base = f"{scheme}://{host}:{port}"
# Step 1: is there a HEAD file, and does it look like a git HEAD? head = get(f"{base}/.git/HEAD") if not head: return []
ref_match = re.match(r"^ref:\s+(refs/[\w./-]+)\s*$", head.strip()) if not ref_match: return [] ref = ref_match.group(1)
# Step 2: follow the ref HEAD named. A server that returns a soft-404 body # for every path fails here, which is the point: the second request is # chosen by the first response's content. ref_body = get(f"{base}/.git/{ref}") if not ref_body: return []
commit = ref_body.strip()
# Step 3: the ref must resolve to a 40-character object ID. This is what # separates a real repository from a page that happens to contain the word # "ref:". if not re.fullmatch(r"[0-9a-f]{40}", commit): return []
return [ { "template_id": "exposed-git-head-verified", "template_name": "Exposed .git directory (verified)", "matched_at": f"{base}/.git/{ref}", "severity": "high", "confidence": 95, "title": "Exposed .git directory", "description": ( f"{base}/.git/ is served. HEAD points at {ref}, which resolves " f"to commit {commit}. Source history is retrievable." ), "evidence": { "request": f"GET {base}/.git/{ref}", "response": commit, "matched_patterns": ["ref:", "40-hex object id"], "data": {"head": head.strip(), "ref": ref, "commit": commit}, }, "cwe": "CWE-527", "remediation": "Stop serving the .git directory. Block /.git/ at the web server or reverse proxy.", "references": ["https://cwe.mitre.org/data/definitions/527.html"], } ]template_name and matched_at are worth setting: cxg template validate
flags findings that omit them.
The entry point
Section titled “The entry point”Read the target from the environment when cxg is calling, and from argv when
you are testing by hand. Supporting both is what makes the template debuggable.
def main(): host = os.environ.get("CERT_X_GEN_TARGET_HOST") port = int(os.environ.get("CERT_X_GEN_TARGET_PORT", "80"))
if not host: if len(sys.argv) < 2: sys.exit("usage: git-head-exposed.py <host> [port]") host = sys.argv[1] if len(sys.argv) > 2: port = int(sys.argv[2])
findings = scan(host, port) print(json.dumps(findings, indent=2))
if __name__ == "__main__": main()Run it standalone
Section titled “Run it standalone”Before involving cxg, check the template works on its own. This is the fastest loop you will have.
python3 git-head-exposed.py 127.0.0.1 8000[ { "template_id": "exposed-git-head-verified", "template_name": "Exposed .git directory (verified)", "matched_at": "http://127.0.0.1:8000/.git/refs/heads/master", "severity": "high", "confidence": 95, "title": "Exposed .git directory", "description": "http://127.0.0.1:8000/.git/ is served. HEAD points at refs/heads/master, which resolves to commit 2b02d7659178aee5532b0a9471af692c8897a1e2. Source history is retrievable.", "evidence": { "request": "GET http://127.0.0.1:8000/.git/refs/heads/master", "response": "2b02d7659178aee5532b0a9471af692c8897a1e2", "matched_patterns": [ "ref:", "40-hex object id" ], "data": { "head": "ref: refs/heads/master", "ref": "refs/heads/master", "commit": "2b02d7659178aee5532b0a9471af692c8897a1e2" } }, "cwe": "CWE-527", "remediation": "Stop serving the .git directory. Block /.git/ at the web server or reverse proxy.", "references": [ "https://cwe.mitre.org/data/definitions/527.html" ] }]Your commit hash will differ.
Validate it
Section titled “Validate it”cxg template validate git-head-exposed.py════════════════════════════════════════════════════════════════════════════════CERT-X-GEN Template Validator════════════════════════════════════════════════════════════════════════════════
Found 1 template(s) to validate
✓ git-head-exposed.py
════════════════════════════════════════════════════════════════════════════════Validation Summary════════════════════════════════════════════════════════════════════════════════
Total Templates: 1 ✓ Passed: 1 ✗ Failed: 0 Success Rate: 100%The validator also reports [info] lines under the template name when something
is worth knowing anyway: a missing template_name, or a print() that is not
json.dumps(). Those do not fail the template, but each one is a real trap and
worth clearing.
Run it through cxg
Section titled “Run it through cxg”Put the template in a directory and point a scan at it.
mkdir -p my-templatescp git-head-exposed.py my-templates/cxg scan --scope http://127.0.0.1:8000 --template-dir ./my-templates════════════════════════════════════════════════════════════════════════════════Scan Summary════════════════════════════════════════════════════════════════════════════════
Scan ID: 90229b6b-315f-496b-aebe-e9463a4a9491 Duration: 0.30s Targets Scanned: 1 Templates Executed: 1
Findings by Severity: CRITICAL: 0 HIGH: 1 MEDIUM: 0 LOW: 0 INFO: 0
TOTAL: 1
════════════════════════════════════════════════════════════════════════════════--template-dir replaces the normal discovery locations, so only your template
runs. Check the recorded finding in scan-results.json:
python3 -c "import json;print(json.load(open('scan-results.json'))['findings'][0]['description'])"http://127.0.0.1:8000/.git/ is served. HEAD points at refs/heads/master, which resolves to commit 2b02d7659178aee5532b0a9471af692c8897a1e2. Source history is retrievable.Prove the verification is real
Section titled “Prove the verification is real”A detection that always fires is not a detection. Take the .git directory away
and scan again.
mv .git .git-hiddencxg scan --scope http://127.0.0.1:8000 --template-dir ./my-templatesFindings by Severity: CRITICAL: 0 HIGH: 0 MEDIUM: 0 LOW: 0 INFO: 0
TOTAL: 0Restore it with mv .git-hidden .git.
This is the test worth repeating for anything you write. Run it against a target that should not match, and confirm it stays quiet. A template that has only ever been run against a vulnerable target has not been tested.
Clean up
Section titled “Clean up”Stop the Python server with Ctrl+C, then remove the directory.
cd ..rm -rf cxg-first-templateWhat to change next
Section titled “What to change next”-
Report more precisely. Set
cvss_scoreandremediationin the finding. They flow straight intoscan-results.jsonand every report format. -
Go one step further. Fetch
.git/configand include theurlof theoriginremote in the evidence. It is one more dependent request, and it tells a reader which repository was exposed. -
Try another language. The same contract applies to all twelve: an annotation header, environment variables in, a JSON array out.
cxg template skeleton goprints the Go scaffold. -
Read the templates that ship with cxg. The set installed on your machine comes from cert-x-gen-templates, where templates are grouped by subject into
web,network,databases,devops,recon, and others. Reading a few that target something close to your own check is the quickest way to pick up the conventions, and the repository’s template writing guide covers template anatomy language by language. -
Contribute it. If your check would be useful to other people, CONTRIBUTING.md sets out the quality standards a submitted template has to meet, which directory it belongs in, and the fork, validate, and pull request workflow.
Related
Section titled “Related”- Why polyglot templates covers when this is worth the cost, and what the cost is.
cxg templatedocuments every template subcommand.cxg scandocuments every scan flag.- cert-x-gen-templates is the official template set, and where to send one you want to share.

