Fuzz Harnesses
Paths.
analysis-results/…andprogress-tracker/…in this skill are the default workspace layout. They resolve throughlocations.yamlin$TRAUST_CONFIG_HOME(docs/setup.md, Storage locations); substitute your configured roots.
Author and run offline Go-native fuzz tests against pure-function attack
surface (parsers, decoders, templaters, regex matchers) that a
secure-code-audit report has flagged as fuzz-worthy. Every harness is a
white-box *_fuzz_test.go copied into the target repo's own package tree so
it can reach unexported identifiers, driven by the Go 1.18+ native fuzzer
(go test -fuzz).
This skill wraps the existing tooling in this directory — targets.json,
Makefile, harnesses/<id>/*_fuzz_test.go, and the batch-plan docs. Read
README.md alongside this file for the full layout and design
notes.
Input
$ARGUMENTS is one of:
- A repository URL or
<org>/<repo>— scaffold new harnesses for that repo. Look up its audit report underanalysis-results/findings/**/<repo>/<repo>-security-audit.{json,md}to find recommended fuzz entry points (findings whoseremediationmentions fuzzing, or the report's remediation-roadmap "add fuzz harness" items). Also check the refuted register —<repo>-refuted-register.jsonalongside the audit report (emitted by python3 -m traust.cli ledger emit-triage) lists findings dismissed as false positives; entries in fuzzable classes (parsers, decoders, memory safety, injection) are PRIORITY targets, because a reproducing crash overrides the FP assertion via evidence-class precedence (fp_overridden). An FP is a falsifiable claim, not an exclusion. - A finding ID (e.g.
KUBE_RBAC_PROXY-9cb7556-019) — scaffold a harness for that specific finding's entry point. 2b. An impact-analysis artifact (analysis-results/impact/<cve>-impact-analysis.json, from/impact-analysis) — scaffold harnesses for the repos classifiedaffected. This is the assurance-promotion path:affectedis call-graph reachability (machine static, evidence class 3); a reproducing input is execution evidence (class 1) that flips the finding toconfirmedin the disposition ledger. Target the entry point from the repo'sevidence.govulncheck_trace— the first in-repo frame is the natural harness entry — and seed the corpus with advisory-shaped input (metadata.feature_descriptionnames the vulnerable capability). Set the target'saudit_refto the CVE ID. Prioritize by severity and byattacker_influence: plausiblewhere triage has judged the trace. - A target ID already in
targets.json— skip authoring; justmake clone install build fuzz-<id>and triage results. - Nothing — run the whole current batch:
make clone install buildthenmake FUZZTIME=<duration> fuzz-all(orBATCH=<n> fuzz-all).
Non-Go languages (C4)
Go stays the default (language absent = go; nothing changes for
existing targets). For other ecosystems, set "language" on the target
and author from the matching template in harnesses/_templates/:
| language | engine | template | run |
|---|---|---|---|
python |
atheris (Apache-2.0) | python_fuzz.py |
make fuzz-<id> — per-clone .fuzzvenv, FUZZTIME-bounded |
javascript |
Jazzer.js (Apache-2.0) | js_fuzz.js |
make fuzz-<id> — npx jazzer, FUZZTIME-bounded |
rust |
cargo-fuzz + libFuzzer | rust_fuzz.rs |
make fuzz-<id> — requires nightly toolchain; dest under fuzz/fuzz_targets/ |
java |
jazzer (JVM), pinned v0.30.0 (sha256-verified download; Apache-2.0 verified at the tag — docs/external-dependencies.md) | JavaFuzz.java |
make fuzz-<id> — builds the target (maven package or gradle assemble), assembles the classpath, runs the pinned jazzer against the manifest's .fuzz class, FUZZTIME-bounded |
Same rules as Go targets: harnesses live in analysis-results/fuzz-harnesses/<id>/ and are
copied into the clone by make install (dest inside the target tree so
imports resolve); expected parse errors are swallowed in the harness —
crashes, hangs, and memory blowups are the findings; crashers are
triaged identically (reproducer + follow-up finding + rollup entry, with
the target's language carried through). Engine licenses are verified
in docs/external-dependencies.md; the engines are dependencies of the
generated harnesses in target repos, never of this repository.
Preconditions
Working directory must be this skill's directory:
cd traust/harnessing/6-fuzz/create-fuzzing
Requires: go ≥ 1.22, jq, git, network access for make clone only.
Workflow
1. Locate or add the target
Check whether $ARGUMENTS already has an entry:
jq -r '.targets[] | select(.id=="<id>" or (.repo | contains("<repo>")))' targets.json
If absent, append a new object to .targets[] in targets.json:
{
"id": "<repo-name>",
"repo": "https://github.com/<org>/<repo>",
"module": "<go module path from go.mod>",
"commit": "<7-or-40-char SHA from the audit report's metadata.commit, or HEAD>",
"product_reach": <int — number of products shipping this repo, from repo-graph>,
"audit_ref": "<finding ID that recommended fuzzing>",
"batch": <next batch number>,
"harnesses": [
{
"file": "<id>/<name>_fuzz_test.go",
"dest": "<pkg-path>/<name>_fuzz_test.go",
"fuzz": "Fuzz<Name>",
"pkg": "./<pkg-path>"
}
]
}
Optional per-target keys the Makefile honours: clone_alias (share a clone
across targets), workdir (subdir containing go.mod), goos (skip on other
host OS).
2. Author the harness
Create analysis-results/fuzz-harnesses/<id>/<name>_fuzz_test.go. Rules:
packagemust match the target package (white-box, so unexported identifiers are reachable).destintargets.jsonplaces the file in-tree.- Seed corpus via
f.Add(...). Prefer real fixtures the repo already ships (docs/samples/*.yaml,testdata/*). Otherwise hand-roll minimal-valid + minimal-invalid + known-hostile (YAML anchor bomb, deeply nested JSON, oversized varint,../path,%nformat string) seeds. - No network, cluster, or cloud at fuzz time. Substitute
k8s.io/client-go/dynamic/fake,discovery/fake, in-memoryio.Reader, syntheticnet.Addr. If the entry point insists on I/O, drive the layer below it. - Prefer exported entry points; if the target is unexported, keep the harness as thin as possible around it so upstream refactors surface as a compile error, not a silent no-op.
- Header comment: drop-in
destpath,go test -fuzzinvocation, target file:line, and the crash classes you expect.
See analysis-results/fuzz-harnesses/kube-rbac-proxy/config_fuzz_test.go for a reference harness
and BATCH-*.md for pattern guidance (sprig/text/template DoS,
strings.Split(...)[N] index panics, differential invariants).
3. Compile-check
make clone # shallow-fetch repos at pinned commits (idempotent)
make install build # copy harnesses in-tree and go test -run '^$' compile-check
make build must pass before fuzzing. A compile error means the pinned
commit's internals moved — re-scout the entry point and update the harness,
do not bump commit to HEAD just to make it compile.
4. Fuzz
make FUZZTIME=10m fuzz-<id> # one target
make FUZZTIME=1h BATCH=<n> fuzz-all # a whole batch
Crashers land in
clones/<id>/<pkg>/testdata/fuzz/<FuzzName>/<hash>.
5. Triage crashers
For each crasher:
go test -run <FuzzName>/<hash> ./<pkg>to reproduce deterministically.Minimise the input (the Go fuzzer usually already has).
Classify: panic (index-OOB, nil deref, type assertion), hang/timeout (CWE-770 resource exhaustion), or invariant violation (differential).
Copy the reproducer into
analysis-results/findings/**/<id>/fuzz-corpus/<FuzzName>/.File a follow-up finding — either amend the source
*-security-audit.jsonwith a new finding whoseevidenceembeds the reproducer and whosevalidation_statusisconfirmed, or open a new finding via/file-security-defect. Use the campaign ID format{REPO_SLUG}-{SHORTSHA}-{NNN}and setsource_findingsto the fuzz target'saudit_ref.Record it in the disposition ledger. A crasher that only exists as prose in a batch summary is not recorded — the ledger is the register of what we know about a finding, and for months it held none of these.
python3 harnessing/6-fuzz/create-fuzzing/scripts/emit_fuzz_events.py # dry run python3 harnessing/6-fuzz/create-fuzzing/scripts/emit_fuzz_events.py --applyOne event per crasher:
source.type: fuzz_report, the bug record assource.ref, the reproducer directory inevidence_refs. Where the target'saudit_refis a cleanFIND-NNNthe event attaches to that finding; otherwise it carries a new one. Emits under gate A15 — the baseline audit report is never written — and is idempotent byevent_id.fuzz_reportis evidence class 1 because the triggering input is kept, so the claim is replayable. The converse is the part that gets misread: a fuzz run that finds nothing emits NOTHING here. Absence of a crash is not evidence of correctness.A repo audited under several products has one layer per product. The tool refuses to guess which and reports the ambiguity;
--all-layersrecords the crasher under every product that ships the code.
6. Report (batch mode)
When running a full batch, run-sweep.sh and gen-reports.sh produce a
per-batch summary under this directory. Update the relevant BATCH-<n>-PLAN.md
with results (hit / miss / compile-gap per target) so the next batch's
selection can learn from it.
Output
| Artefact | Path |
|---|---|
| New/updated manifest entry | targets.json |
| Harness source | analysis-results/fuzz-harnesses/<id>/*_fuzz_test.go — the CORPUS, not this repo: a harness is test code written ABOUT an audited repository, the same category as a finding about it. file: in targets.json is relative to that root (HARNESSES in the Makefile) |
| Reproducers | analysis-results/findings/**/<id>/fuzz-corpus/<FuzzName>/<hash> |
| Crashers (git-ignored) | clones/<id>/<pkg>/testdata/fuzz/<FuzzName>/ |
| Follow-up finding | carried on a ledger event in analysis-results/findings/**/<repo>-findings-layer.json (gate A15 — never the baseline) |
Campaign summary + rollup (via build_fuzz_rollup.py) |
analysis-results/FUZZ-CAMPAIGN-SUMMARY.{md,json} + FUZZ-FINDINGS-ROLLUP.md — the .json is the machine-readable sidecar dashboards consume; all three also published to progress-tracker/metrics/dashboards/fuzz/ |
No JSON schema validation step — this skill produces Go source and manifest edits, not audit reports. The report artefact is the follow-up finding carried on its ledger event; the originating audit report is not modified (gate A15).
Integrations
- Inputs: audit reports' fuzz-worthy entry points, refuted registers
(FP falsification targets), and
/impact-analysisartifacts (form 2b — the class-3→class-1 promotion path). - Outputs consumed by: the event-carried follow-up finding
(with reproducer evidence) flows through
/track-findingsas execution evidence;FUZZ-CAMPAIGN-SUMMARY.jsonfeeds/validation-fuzz-dashboard; crashers override FP assertions via evidence-class precedence in the disposition ledger.
Spend declaration (calibration tuple)
After this skill's report/artifact is written, declare the run's spend
against the target so estimate_scan can calibrate per-skill cost
models (contract: docs/model-routing.md; analysis:
progress-tracker/metrics/estimate-calibration-analysis.md §F5):
python3 -m traust.cli registry models spend --skill create-fuzzing \
--model <resolved model id> [--tokens-in <N>] [--tokens-out <N>] \
--repo <target-slug> --loc <target size, if known> [--batch <batch-id>]
Token counts are OPTIONAL and best-effort: pass them when the orchestrator has them (Task results carry per-subagent usage), otherwise omit them — an agent cannot observe its own usage mid-run. This row is a routing marker, not a cost claim; actual per-lane cost is attributed from session transcripts by python3 -m traust.cli metrics attribute-spend. Never skip the row: an unattributed run is a calibration gap.