mirror of
https://github.com/enix/x509-certificate-exporter.git
synced 2026-08-28 16:27:16 +00:00
test(fuzz): new tests on cert parsers and glob pattern
This commit is contained in:
@@ -52,8 +52,9 @@ everything.
|
||||
| Lint Helm | `task lint:helm` | `dagger call lint-helm` |
|
||||
| Lint Markdown | `task lint:markdown` | `dagger call lint-markdown` |
|
||||
| Lint all | `task lint` | Go + Helm + Renovate + Markdown |
|
||||
| All tests | `task test` | runs `test:unit` + `test:e2e` sequentially |
|
||||
| All tests | `task test` | runs `test:unit` + `test:fuzz` + `test:e2e` sequentially |
|
||||
| Unit tests | `task test:unit` | `dagger call test` — gotestsum + `-race` + coverage |
|
||||
| Fuzz smoke | `task test:fuzz` | each `Fuzz*` target run for 5s — catches seed-corpus regressions |
|
||||
| End-to-end tests | `task test:e2e` | throwaway k3d cluster, Helm install, scrape `/metrics` |
|
||||
| Vuln scan | `task security:govulncheck` | `dagger call govulncheck` |
|
||||
| Vuln scan (deps) | `task security:vuln-deps` | `dagger call trivy --scan-type=fs` |
|
||||
@@ -198,6 +199,11 @@ the pod's state or making HTTP requests to it right after modifying source files
|
||||
from `internal/` without a real consumer.
|
||||
- Tests use the standard `*_test.go` colocated layout. Prefer table-driven tests
|
||||
for the parser/registry packages.
|
||||
- Parsers that consume untrusted bytes (PEM, PKCS#12, fileglob patterns) carry
|
||||
`Fuzz*` targets in `*_fuzz_test.go` files. `task test:fuzz` runs each for 5s
|
||||
(smoke); for a real session use `go test -fuzz=<name> -fuzztime=10m ./<pkg>`.
|
||||
A crash gets persisted under `testdata/fuzz/...` and becomes a regression
|
||||
test — commit those files alongside the fix.
|
||||
- Version metadata is injected at build time via `-ldflags -X` into
|
||||
`internal/product`. Don't read it from env or from disk at runtime.
|
||||
- The `formatVersion` short form is `MAJOR.MINOR.PATCH+gSHORTSHA` and gets a
|
||||
|
||||
+3
-2
@@ -207,7 +207,7 @@ task --list # show every available task with description
|
||||
task build # `go build` directly (host-arch only)
|
||||
task image # `goreleaser release --snapshot ...`
|
||||
task lint # → go run ./dagger lint:go (+helm, +renovate)
|
||||
task test # runs test:unit then test:e2e
|
||||
task test # runs test:unit, test:fuzz, test:e2e
|
||||
```
|
||||
|
||||
Whenever you're not sure what command to run, `task --list` is the answer.
|
||||
@@ -597,8 +597,9 @@ releases.
|
||||
| `task dev:up` | Tilt up — full dev loop |
|
||||
| `task dev:down` | Tilt down — keep cluster |
|
||||
| `task dev:cluster:down` | Destroy dev cluster + registry |
|
||||
| `task test` | Run unit + e2e tests sequentially |
|
||||
| `task test` | Run unit + fuzz smoke + e2e tests sequentially |
|
||||
| `task test:unit` | Unit tests with race detector + coverage (Dagger) |
|
||||
| `task test:fuzz` | Smoke-run every `Fuzz*` target (5s each) |
|
||||
| `task test:e2e` | Full e2e against fresh throwaway cluster |
|
||||
| `task lint` | All linters (Go + Helm + Renovate) |
|
||||
| `task lint:{go,gocritic,gonocritic,helm,renovate}` | Single linter (Dagger) |
|
||||
|
||||
@@ -189,6 +189,7 @@ tasks:
|
||||
desc: 'Run all tests (unit + e2e)'
|
||||
cmds:
|
||||
- task: test:unit
|
||||
- task: test:fuzz
|
||||
- task: test:e2e
|
||||
|
||||
test:unit:
|
||||
@@ -196,6 +197,16 @@ tasks:
|
||||
cmds:
|
||||
- dagger call --progress tty test
|
||||
|
||||
test:fuzz:
|
||||
desc: 'Smoke-run every fuzzer (5s each) — fails on any new crash'
|
||||
# Go fuzz only targets one Fuzz* function at a time, so we list each
|
||||
# explicitly. Long-form fuzzing is a manual local activity:
|
||||
# go test -fuzz=<name> -fuzztime=10m ./<pkg>
|
||||
cmds:
|
||||
- go test -run=^$ -fuzz=FuzzParse -fuzztime=5s ./pkg/cert/pem/
|
||||
- go test -run=^$ -fuzz=FuzzParse -fuzztime=5s ./pkg/cert/pkcs12/
|
||||
- go test -run=^$ -fuzz=FuzzCompile -fuzztime=5s ./internal/fileglob/
|
||||
|
||||
test:e2e:
|
||||
desc: 'End-to-end against a throwaway cluster — built and torn down by the task'
|
||||
vars:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package fileglob
|
||||
|
||||
import "testing"
|
||||
|
||||
// FuzzCompile exercises the pattern compiler on arbitrary strings.
|
||||
// Contract: Compile must never panic. Malformed character classes,
|
||||
// unterminated brackets, weird metacharacters — all must return an error,
|
||||
// never a crash. A successfully-compiled pattern must also Match without
|
||||
// panic on arbitrary targets.
|
||||
func FuzzCompile(f *testing.F) {
|
||||
for _, seed := range []string{
|
||||
"",
|
||||
"/etc/kubernetes/pki/*.crt",
|
||||
"/var/**/*.pem",
|
||||
"/etc/[abc]*.crt",
|
||||
"/etc/[a-z]*.crt",
|
||||
"[", // unterminated class
|
||||
"[!]", // empty inverted class
|
||||
"[z-a]", // inverted range
|
||||
"**/**", // double recursive
|
||||
"a\\b", // backslash
|
||||
"{a,b}", // brace
|
||||
"\xff\x00\x01", // binary garbage
|
||||
} {
|
||||
f.Add(seed)
|
||||
}
|
||||
|
||||
f.Fuzz(func(_ *testing.T, pattern string) {
|
||||
p, err := Compile(pattern)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = p.Match("/etc/kubernetes/pki/ca.crt")
|
||||
_ = p.Match("")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package pem
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/cert"
|
||||
)
|
||||
|
||||
// FuzzParse exercises the PEM parser on arbitrary bytes. Contract: Parse
|
||||
// must never panic, regardless of input. Truncated headers, garbage
|
||||
// inside CERTIFICATE blocks, mixed block types — all must surface as
|
||||
// Bundle.Errors entries, never as a runtime crash.
|
||||
func FuzzParse(f *testing.F) {
|
||||
for _, seed := range [][]byte{
|
||||
nil,
|
||||
[]byte("garbage"),
|
||||
[]byte("-----BEGIN CERTIFICATE-----\n"),
|
||||
[]byte("-----BEGIN CERTIFICATE-----\nQUFB\n-----END CERTIFICATE-----\n"),
|
||||
[]byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n"),
|
||||
[]byte("-----BEGIN TRUSTED CERTIFICATE-----\nQUFB\n-----END TRUSTED CERTIFICATE-----\n"),
|
||||
[]byte("-----BEGIN PRIVATE KEY-----\nQUFB\n-----END PRIVATE KEY-----\n"),
|
||||
} {
|
||||
f.Add(seed)
|
||||
}
|
||||
|
||||
p := New()
|
||||
ref := cert.SourceRef{Kind: "fuzz", SourceName: "fuzz"}
|
||||
f.Fuzz(func(_ *testing.T, data []byte) {
|
||||
_ = p.Parse(data, ref, cert.ParseOptions{})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package pkcs12
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/cert"
|
||||
)
|
||||
|
||||
// FuzzParse exercises the PKCS#12 parser on arbitrary bytes paired with
|
||||
// arbitrary passphrases. Contract: Parse must never panic — corrupt ASN.1,
|
||||
// wrong-length salt, malformed MAC, etc. must surface as Bundle.Errors,
|
||||
// never as a runtime crash.
|
||||
//
|
||||
// PKCS#12 is a notoriously baroque format (PKCS#12 → PFX → PKCS#7 →
|
||||
// PKCS#8 → ASN.1 DER), so the parser delegates to software.sslmate.com's
|
||||
// implementation. This fuzzer enforces our invariant that even when that
|
||||
// library returns an error or unexpected state, our wrapper stays sane.
|
||||
func FuzzParse(f *testing.F) {
|
||||
for _, data := range [][]byte{
|
||||
nil,
|
||||
{0x30}, // bare ASN.1 SEQUENCE tag
|
||||
{0x30, 0x00},
|
||||
[]byte("not pkcs12"),
|
||||
} {
|
||||
for _, pass := range []string{"", "letmein"} {
|
||||
f.Add(data, pass)
|
||||
}
|
||||
}
|
||||
|
||||
p := New()
|
||||
ref := cert.SourceRef{Kind: "fuzz", SourceName: "fuzz"}
|
||||
f.Fuzz(func(_ *testing.T, data []byte, pass string) {
|
||||
_ = p.Parse(data, ref, cert.ParseOptions{Pkcs12Passphrase: pass})
|
||||
_ = p.Parse(data, ref, cert.ParseOptions{Pkcs12Passphrase: pass, Pkcs12TryEmpty: true})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user