diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 000000000..713c0e7de --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,25 @@ +name: Fuzz + +on: + pull_request: + push: + workflow_dispatch: + +permissions: + contents: read + +jobs: + fuzz-short: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '^1.22' + check-latest: true + cache: true + - name: Run short fuzzing session + run: | + # Run all fuzz targets briefly; -run=^$ disables non-fuzz tests + go test ./... -run=^$ -fuzz=Fuzz -fuzztime=30s + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f356e6a8..1d1e83efa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,16 @@ If you have questions regarding Cobra, feel free to ask it in the community 1. Since this is golang project, ensure the new code is properly formatted to ensure code consistency. Run `make all`. +### Fuzz testing + +Go 1.18+ fuzz tests are included. For a short local fuzz run: + +``` +go test ./... -run=^$ -fuzz=Fuzz -fuzztime=30s +``` + +CI runs a brief fuzz pass on each push/PR. See `site/content/fuzzing.md` for details and OSS-Fuzz onboarding pointers. + ### Quick steps to contribute 1. Fork the project. diff --git a/README.md b/README.md index 8416275f4..4ec3d36c9 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,16 @@ For complete details on using the Cobra-CLI generator, please read [The Cobra Ge For complete details on using the Cobra library, please read [The Cobra User Guide](site/content/user_guide.md). +# Fuzz testing + +Cobra includes Go fuzz tests (Go 1.18+). To run a short fuzzing session locally: + +``` +go test ./... -run=^$ -fuzz=Fuzz -fuzztime=30s +``` + +We are exploring integration with Google's OSS-Fuzz to further harden Cobra. See Go's official fuzzing docs at [`https://go.dev/doc/security/fuzz/`](https://go.dev/doc/security/fuzz/). + # License Cobra is released under the Apache 2.0 license. See [LICENSE.txt](LICENSE.txt) diff --git a/fuzz/cobra_fuzz_test.go b/fuzz/cobra_fuzz_test.go new file mode 100644 index 000000000..d81798651 --- /dev/null +++ b/fuzz/cobra_fuzz_test.go @@ -0,0 +1,88 @@ +//go:build go1.18 +// +build go1.18 + +package cobra + +import ( + "strings" + "testing" +) + +// FuzzLd fuzzes the Levenshtein distance implementation and checks basic invariants. +func FuzzLd(f *testing.F) { + // Seed corpus + f.Add("", "") + f.Add("a", "") + f.Add("", "a") + f.Add("kitten", "sitting") + f.Add("Saturday", "Sunday") + f.Add("MixedCase", "mixedcase") + + f.Fuzz(func(t *testing.T, a string, b string) { + // Distance is always >= 0 + d := ld(a, b, false) + if d < 0 { + t.Fatalf("ld returned negative distance: %d", d) + } + + // Symmetry: ld(a,b) == ld(b,a) + d2 := ld(b, a, false) + if d != d2 { + t.Fatalf("ld not symmetric: ld(%q,%q)=%d ld(%q,%q)=%d", a, b, d, b, a, d2) + } + + // Case-insensitive should be <= case-sensitive for inputs differing only by case + if strings.EqualFold(a, b) { + di := ld(a, b, true) + if di != 0 { + t.Fatalf("ld case-insensitive mismatch for equalFold inputs: %q %q => %d", a, b, di) + } + } + + // Identity: distance is 0 when strings are equal + if a == b && d != 0 { + t.Fatalf("ld identity broken: ld(%q,%q)=%d", a, b, d) + } + }) +} + +// FuzzConfigEnvVar fuzzes configEnvVar to ensure it only produces A-Z0-9_ and is stable for allowed inputs. +func FuzzConfigEnvVar(f *testing.F) { + f.Add("prog", "ACTIVE_HELP") + f.Add("My-App", "COMPLETION_DESCRIPTIONS") + f.Add("", "X") + f.Fuzz(func(t *testing.T, name, suffix string) { + v := configEnvVar(name, suffix) + if v == "" { + t.Fatal("empty env var name") + } + // Must contain only A-Z0-9_ + for i := 0; i < len(v); i++ { + c := v[i] + if !(c == '_' || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { + t.Fatalf("invalid char %q in %q", c, v) + } + } + // Ensure uppercasing behavior for simple alnum inputs + cleanName := name + for i := 0; i < len(cleanName); i++ { + b := cleanName[i] + if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')) { + cleanName = "" + break + } + } + if cleanName != "" { + upper := strings.ToUpper(cleanName + "_" + suffix) + // The sanitizer replaces non A-Z0-9_ with _, which shouldn't occur here. + if v != strings.Map(func(r rune) rune { + if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { + return r + } + return '_' + }, upper) { + t.Fatalf("unexpected mapping for simple input: got %q want %q", v, upper) + } + } + }) +} diff --git a/site/content/fuzzing.md b/site/content/fuzzing.md new file mode 100644 index 000000000..726750b97 --- /dev/null +++ b/site/content/fuzzing.md @@ -0,0 +1,27 @@ +--- +title: Fuzz testing +weight: 90 +--- + +This project includes Go fuzz tests (Go 1.18+). You can run a short fuzz session locally with: + +``` +go test ./... -run=^$ -fuzz=Fuzz -fuzztime=30s +``` + +Continuous Integration runs a short fuzz pass on each push and pull request. + +OSS-Fuzz integration + +- Cobra is prepared for OSS-Fuzz via in-repo `Fuzz*` targets (see `fuzz/`). +- To onboard to OSS-Fuzz, open a PR to the upstream `google/oss-fuzz` repo creating `projects/cobra/` with: + - `project.yaml` referencing Go as the language + - `Dockerfile` installing dependencies and building the fuzz targets + - `build.sh` that runs `compile_go_fuzzer` for each fuzz function (e.g., `FuzzLd`, `FuzzConfigEnvVar`) +- Reference: https://google.github.io/oss-fuzz/getting-started/new-project-guide/ + +Notes + +- Keep fuzz targets small and deterministic with clear invariants. +- Prefer focusing on pure functions and parsers (e.g., helpers like `ld`, env var processing). +