Repository: fluxcd/notification-controller
Pull request #414 (2022-09-06 22:05:30)

fuzz: Refactor Fuzzers based on Go native fuzzing
The existing fuzzers were converted into the Go native format. Based on how the code was structured on this project, the fuzzers can be quite effective, allowing for entire E2E fuzzing in some cases, but with very low execution cost.

Relates to https://github.com/fluxcd/flux2/issues/2417.


Patch:
diff --git a/Makefile b/Makefile
index 5459fa1..fc31a4d 100644
--- a/Makefile
+++ b/Makefile
@@ -19,6 +19,10 @@ BUILD_PLATFORMS ?= linux/amd64
 # Architecture to use envtest with
 ENVTEST_ARCH ?= amd64
 
+# FUZZ_TIME defines the max amount of time, in Go Duration,
+# each fuzzer should run for.
+FUZZ_TIME ?= 1m
+
 all: manager
 
 # Run tests
@@ -109,7 +113,7 @@ docker-push:
 docker-deploy:
 	kubectl -n flux-system set image deployment/notification-controller manager=${IMG}
 
-# Build fuzzers
+# Build fuzzers used by oss-fuzz.
 fuzz-build:
 	rm -rf $(shell pwd)/build/fuzz/
 	mkdir -p $(shell pwd)/build/fuzz/out/
@@ -122,7 +126,7 @@ fuzz-build:
 		-v "$(shell pwd)/build/fuzz/out":/out \
 		local-fuzzing:latest
 
-# Run each fuzzer once to ensure they are working
+# Run each fuzzer once to ensure they will work when executed by oss-fuzz.
 fuzz-smoketest: fuzz-build
 	docker run --rm \
 		-v "$(shell pwd)/build/fuzz/out":/out \
@@ -130,6 +134,12 @@ fuzz-smoketest: fuzz-build
 		local-fuzzing:latest \
 		bash -c "/runner.sh"
 
+# Run fuzz tests for the duration set in FUZZ_TIME.
+fuzz-native: 
+	KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \
+	FUZZ_TIME=$(FUZZ_TIME) \
+		./tests/fuzz/native_go_run.sh
+
 # Find or download controller-gen
 CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
 .PHONY: controller-gen
diff --git a/go.mod b/go.mod
index fe794d1..e4e9ce4 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.18
 replace github.com/fluxcd/notification-controller/api => ./api
 
 require (
+	github.com/AdaLogics/go-fuzz-headers v0.0.0-20220903154154-e8044f6e4c72
 	github.com/Azure/azure-amqp-common-go/v3 v3.2.3
 	github.com/Azure/azure-event-hubs-go/v3 v3.3.18
 	github.com/containrrr/shoutrrr v0.6.1
@@ -68,6 +69,7 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
+	github.com/cyphar/filepath-securejoin v0.2.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/devigned/tab v0.1.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.8.0 // indirect
diff --git a/go.sum b/go.sum
index 7016498..eaf963c 100644
--- a/go.sum
+++ b/go.sum
@@ -46,6 +46,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20220903154154-e8044f6e4c72 h1:1sCHCT0xRr7UArrI1WJxsl9S8QeYdf0fmuGIl2xb7YI=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20220903154154-e8044f6e4c72/go.mod h1:i9fr2JpcEcY/IHEvzCM3qXUZYOQHgR89dt4es1CgMhc=
 github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
 github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas=
 github.com/Azure/azure-event-hubs-go/v3 v3.3.18 h1:jgWDk2qmknA0UsfyzjHiW5yciOw3aBY0Oq9p/M9lz2Q=
@@ -169,6 +171,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
+github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -450,6 +454,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -1151,6 +1156,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
diff --git a/internal/notifier/azure_devops.go b/internal/notifier/azure_devops.go
index 93313d7..8bb8e6c 100644
--- a/internal/notifier/azure_devops.go
+++ b/internal/notifier/azure_devops.go
@@ -147,7 +147,16 @@ func toAzureDevOpsState(severity string) (git.GitStatusState, error) {
 // duplicateStatus return true if the latest status
 // with a matching context has the same state and description
 func duplicateAzureDevOpsStatus(statuses *[]git.GitStatus, status *git.GitStatus) bool {
+	if status == nil || status.Context == nil || statuses == nil {
+		return false
+	}
+
 	for _, s := range *statuses {
+		if s.Context == nil || s.Context.Name == nil || s.Context.Genre == nil ||
+			s.State == nil || s.Description == nil {
+			continue
+		}
+
 		if *s.Context.Name == *status.Context.Name && *s.Context.Genre == *status.Context.Genre {
 			if *s.State == *status.State && *s.Description == *status.Description {
 				return true
diff --git a/internal/notifier/github.go b/internal/notifier/github.go
index 58b3fe6..57adb41 100644
--- a/internal/notifier/github.go
+++ b/internal/notifier/github.go
@@ -144,7 +144,15 @@ func toGitHubState(severity string) (string, error) {
 // duplicateStatus return true if the latest status
 // with a matching context has the same state and description
 func duplicateGithubStatus(statuses []*github.RepoStatus, status *github.RepoStatus) bool {
+	if status == nil || statuses == nil {
+		return false
+	}
+
 	for _, s := range statuses {
+		if s.Context == nil || s.State == nil || s.Description == nil {
+			continue
+		}
+
 		if *s.Context == *status.Context {
 			if *s.State == *status.State && *s.Description == *status.Description {
 				return true


Test patch:
diff --git a/internal/notifier/alertmanager_test.go b/internal/notifier/alertmanager_test.go
index 432ecd3..62041ac 100644
--- a/internal/notifier/alertmanager_test.go
+++ b/internal/notifier/alertmanager_test.go
@@ -18,13 +18,18 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
+
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
 )
 
 func TestAlertmanager_Post(t *testing.T) {
@@ -44,3 +49,42 @@ func TestAlertmanager_Post(t *testing.T) {
 	err = alertmanager.Post(context.TODO(), testEvent())
 	require.NoError(t, err)
 }
+
+func Fuzz_AlertManager(f *testing.F) {
+	f.Add("update", "", "", []byte{}, []byte("{}"))
+	f.Add("something", "", "else", []byte{}, []byte(""))
+
+	f.Fuzz(func(t *testing.T,
+		commitStatus, urlSuffix, summary string, seed, response []byte) {
+
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		alertmanager, err := NewAlertmanager(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", &cert)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+
+		// Try to fuzz the event object, but if it fails (not enough seed),
+		// ignore it, as other inputs are also being used in this test.
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil && (commitStatus != "" || summary != "") {
+			event.Metadata = map[string]string{
+				"commit_status": commitStatus,
+				"summary":       summary,
+			}
+		}
+
+		_ = alertmanager.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/azure_devops_test.go b/internal/notifier/azure_devops_test.go
index 75c49f0..f6ad56a 100644
--- a/internal/notifier/azure_devops_test.go
+++ b/internal/notifier/azure_devops_test.go
@@ -17,10 +17,20 @@ limitations under the License.
 package notifier
 
 import (
+	"context"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
 	"testing"
 
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/microsoft/azure-devops-go-api/azuredevops/v6/git"
 	"github.com/stretchr/testify/assert"
+
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
 )
 
 func TestNewAzureDevOpsBasic(t *testing.T) {
@@ -60,6 +70,53 @@ func TestDuplicateAzureDevOpsStatus(t *testing.T) {
 	}
 }
 
+const apiLocations = `{"count":0,"value":[{"area":"","id":"428dd4fb-fda5-4722-af02-9313b80305da","routeTemplate":"","resourceName":"","maxVersion":"6.0","minVersion":"5.0","releasedVersion":"6.0"}]}`
+
+func Fuzz_AzureDevOps(f *testing.F) {
+	f.Add("alakazam", "org/proj/_git/repo", "revision/dsa123a", "error", "", []byte{}, []byte(`{"count":1,"value":[{"state":"error","description":"","context":{"genre":"fluxcd","name":"/"}}]}`))
+	f.Add("alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":1,"value":[{"state":"info","description":"","context":{"genre":"fluxcd","name":"/"}}]}`))
+	f.Add("alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":0,"value":[]}`))
+	f.Add("alakazam", "org/proj/_git/repo", "", "", "Progressing", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, urlSuffix, revision, severity, reason string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if strings.HasSuffix(r.URL.Path, "_apis") {
+				w.Write([]byte(apiLocations))
+			} else {
+				w.Write(response)
+			}
+
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		azureDevOps, err := NewAzureDevOps(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+
+		// Try to fuzz the event object, but if it fails (not enough seed),
+		// ignore it, as other inputs are also being used in this test.
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil && (revision != "") {
+			event.Metadata = map[string]string{
+				"revision": revision,
+			}
+		}
+		event.Severity = severity
+
+		_ = azureDevOps.Post(context.TODO(), event)
+	})
+}
+
 func azStatus(state git.GitStatusState, context string, description string) *git.GitStatus {
 	genre := "fluxcd"
 	return &git.GitStatus{
diff --git a/internal/notifier/bitbucket_test.go b/internal/notifier/bitbucket_test.go
index 519506e..69d9470 100644
--- a/internal/notifier/bitbucket_test.go
+++ b/internal/notifier/bitbucket_test.go
@@ -17,8 +17,17 @@ limitations under the License.
 package notifier
 
 import (
+	"context"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -38,3 +47,49 @@ func TestNewBitbucketInvalidToken(t *testing.T) {
 	_, err := NewBitbucket("https://bitbucket.org/foo/bar", "bar", nil)
 	assert.NotNil(t, err)
 }
+
+func Fuzz_Bitbucket(f *testing.F) {
+	f.Add("user:pass", "org/repo", "revision/dsa123a", "info", []byte{}, []byte(`{"state":"SUCCESSFUL","description":"","key":"","name":"","url":""}`))
+	f.Add("user:pass", "org/repo", "revision/dsa123a", "error", []byte{}, []byte(`{}`))
+
+	f.Fuzz(func(t *testing.T,
+		token, urlSuffix, revision, severity string, seed, response []byte) {
+
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			io.Copy(io.Discard, r.Body)
+			w.Write(response)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		bitbucket, err := NewBitbucket(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
+		if err != nil {
+			return
+		}
+
+		apiUrl, err := url.Parse(ts.URL)
+		if err != nil {
+			t.Fatalf("cannot parse api base URL: %v", err)
+		}
+		// Ensure the call does not go to bitbucket and fuzzes the response.
+		bitbucket.Client.SetApiBaseURL(*apiUrl)
+
+		event := events.Event{}
+
+		// Try to fuzz the event object, but if it fails (not enough seed),
+		// ignore it, as other inputs are also being used in this test.
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil && (revision != "") {
+			event.Metadata = map[string]string{
+				"revision": revision,
+			}
+		}
+		event.Severity = severity
+
+		_ = bitbucket.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/discord_test.go b/internal/notifier/discord_test.go
index db15eaa..a407e63 100644
--- a/internal/notifier/discord_test.go
+++ b/internal/notifier/discord_test.go
@@ -19,12 +19,15 @@ package notifier
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"strings"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -49,3 +52,37 @@ func TestDiscord_Post(t *testing.T) {
 	err = discord.Post(context.TODO(), testEvent())
 	require.NoError(t, err)
 }
+
+func Fuzz_Discord(f *testing.F) {
+	f.Add("username", "channel", "/slack", "info", "update", []byte{}, []byte("{}"))
+	f.Add("", "channel", "", "error", "", []byte{}, []byte(""))
+
+	f.Fuzz(func(t *testing.T,
+		username, channel, urlSuffix, severity, commitStatus string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			io.Copy(io.Discard, r.Body)
+			w.Write(response)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		discord, err := NewDiscord(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", username, channel)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		// Try to fuzz the event object, but if it fails (not enough seed),
+		// ignore it, as other inputs are also being used in this test.
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+		event.Severity = severity
+
+		_ = discord.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/forwarder_test.go b/internal/notifier/forwarder_test.go
index c3d97cc..eb4dd66 100644
--- a/internal/notifier/forwarder_test.go
+++ b/internal/notifier/forwarder_test.go
@@ -18,12 +18,15 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
 	"github.com/fluxcd/pkg/runtime/events"
 
 	"github.com/stretchr/testify/require"
@@ -52,3 +55,33 @@ func TestForwarder_Post(t *testing.T) {
 	err = forwarder.Post(context.TODO(), testEvent())
 	require.NoError(t, err)
 }
+
+func Fuzz_Forwarder(f *testing.F) {
+	f.Add("", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		urlSuffix string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		header := make(map[string]string)
+		_ = fuzz.NewConsumer(seed).FuzzMap(&header)
+
+		forwarder, err := NewForwarder(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", header, &cert)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		_ = forwarder.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/github_dispatch_test.go b/internal/notifier/github_dispatch_test.go
index 0841f7d..5cdd633 100644
--- a/internal/notifier/github_dispatch_test.go
+++ b/internal/notifier/github_dispatch_test.go
@@ -18,8 +18,15 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -59,3 +66,40 @@ func TestGitHubDispatch_PostUpdate(t *testing.T) {
 	err = githubDispatch.Post(context.TODO(), event)
 	require.NoError(t, err)
 }
+
+func Fuzz_GitHub_Dispatch(f *testing.F) {
+	f.Add("token", "org/repo", "", []byte{}, []byte{})
+	f.Add("token", "org/repo", "update", []byte{}, []byte{})
+	f.Add("", "", "", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, urlSuffix, commitStatus string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		dispatch, err := NewGitHubDispatch(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		// Try to fuzz the event object, but if it fails (not enough seed),
+		// ignore it, as other inputs are also being used in this test.
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+
+		_ = dispatch.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/github_test.go b/internal/notifier/github_test.go
index c320df1..3ded5a0 100644
--- a/internal/notifier/github_test.go
+++ b/internal/notifier/github_test.go
@@ -17,8 +17,16 @@ limitations under the License.
 package notifier
 
 import (
+	"context"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/google/go-github/v41/github"
 	"github.com/stretchr/testify/assert"
 )
@@ -69,6 +77,47 @@ func TestDuplicateGithubStatus(t *testing.T) {
 	}
 }
 
+func Fuzz_GitHub(f *testing.F) {
+	f.Add("token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`))
+	f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"success","description":""}]`))
+	f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`))
+	f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/"}]`))
+	f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{}]`))
+	f.Add("token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, urlSuffix, revision, severity, reason string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		github, err := NewGitHub(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil && (revision != "") {
+			event.Metadata = map[string]string{
+				"revision": revision,
+			}
+		}
+
+		event.Severity = severity
+		event.Reason = reason
+
+		_ = github.Post(context.TODO(), event)
+	})
+}
+
 func ghStatus(state string, context string, description string) *github.RepoStatus {
 	return &github.RepoStatus{
 		State:       &state,
diff --git a/internal/notifier/gitlab_test.go b/internal/notifier/gitlab_test.go
index 32a50bd..5277a53 100644
--- a/internal/notifier/gitlab_test.go
+++ b/internal/notifier/gitlab_test.go
@@ -17,8 +17,16 @@ limitations under the License.
 package notifier
 
 import (
+	"context"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -45,3 +53,42 @@ func TestNewGitLabEmptyToken(t *testing.T) {
 	_, err := NewGitLab("https://gitlab.com/foo/bar", "", nil)
 	assert.NotNil(t, err)
 }
+
+func Fuzz_GitLab(f *testing.F) {
+	f.Add("token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`))
+	f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`))
+	f.Add("token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{})
+	f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[]`))
+
+	f.Fuzz(func(t *testing.T,
+		token, urlSuffix, revision, severity, reason string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		gitLab, err := NewGitLab(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil && (revision != "") {
+			event.Metadata = map[string]string{
+				"revision": revision,
+			}
+		}
+
+		event.Severity = severity
+		event.Reason = reason
+
+		_ = gitLab.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/google_chat_test.go b/internal/notifier/google_chat_test.go
index 8ca3074..0060d6a 100644
--- a/internal/notifier/google_chat_test.go
+++ b/internal/notifier/google_chat_test.go
@@ -19,11 +19,14 @@ package notifier
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
 )
 
@@ -49,3 +52,36 @@ func TestGoogleChat_Post(t *testing.T) {
 	err = google_chat.Post(context.TODO(), testEvent())
 	require.NoError(t, err)
 }
+
+func Fuzz_GoogleChat(f *testing.F) {
+	f.Add("", "error", "", "", []byte{}, []byte{})
+	f.Add("", "info", "", "update", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		urlSuffix, severity, message, commitStatus string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		googlechat, err := NewGoogleChat(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "")
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+		event.Message = message
+		event.Severity = severity
+
+		_ = googlechat.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/grafana_test.go b/internal/notifier/grafana_test.go
index 6675cd9..d4765f4 100644
--- a/internal/notifier/grafana_test.go
+++ b/internal/notifier/grafana_test.go
@@ -18,12 +18,16 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -51,3 +55,38 @@ func TestGrafana_Post(t *testing.T) {
 		assert.NoError(t, err)
 	})
 }
+
+func Fuzz_Grafana(f *testing.F) {
+	f.Add("token", "user", "pass", "", "", []byte{}, []byte{})
+	f.Add("", "user", "pass", "", "", []byte{}, []byte{})
+	f.Add("token", "user", "pass", "", "update", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, username, password, urlSuffix, commitStatus string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		grafana, err := NewGrafana(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", token, &cert, username, password)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+
+		_ = grafana.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/lark_test.go b/internal/notifier/lark_test.go
index 50f07ac..83ae6d4 100644
--- a/internal/notifier/lark_test.go
+++ b/internal/notifier/lark_test.go
@@ -3,11 +3,14 @@ package notifier
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
 )
 
@@ -30,3 +33,35 @@ func TestLark_Post(t *testing.T) {
 	err = lark.Post(context.TODO(), testEvent())
 	require.NoError(t, err)
 }
+
+func Fuzz_Lark(f *testing.F) {
+	f.Add("", "", "error", []byte{}, []byte{})
+	f.Add("", "update", "error", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		urlSuffix, commitStatus, severity string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		lark, err := NewLark(fmt.Sprintf("%s/%s", ts.URL, urlSuffix))
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+		event.Severity = severity
+
+		_ = lark.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/matrix_test.go b/internal/notifier/matrix_test.go
index ad84685..cba7f6d 100644
--- a/internal/notifier/matrix_test.go
+++ b/internal/notifier/matrix_test.go
@@ -1,9 +1,16 @@
 package notifier
 
 import (
+	"context"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 	"time"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
 	"github.com/fluxcd/pkg/runtime/events"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -48,3 +55,34 @@ func TestSha1Sum(t *testing.T) {
 		}
 	}
 }
+
+func Fuzz_Matrix(f *testing.F) {
+	f.Add("token", "room1", "", "error", []byte{}, []byte{})
+	f.Add("token", "room1", "", "info", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, roomId, urlSuffix, severity string, seed, response []byte) {
+
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		matrix, err := NewMatrix(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, roomId, &cert)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		event.Severity = severity
+
+		_ = matrix.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/opsgenie_test.go b/internal/notifier/opsgenie_test.go
index 9e5690f..f03fa57 100644
--- a/internal/notifier/opsgenie_test.go
+++ b/internal/notifier/opsgenie_test.go
@@ -18,12 +18,16 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
 )
 
@@ -44,3 +48,38 @@ func TestOpsgenie_Post(t *testing.T) {
 	err = opsgenie.Post(context.TODO(), testEvent())
 	require.NoError(t, err)
 }
+
+func Fuzz_OpsGenie(f *testing.F) {
+	f.Add("token", "", "", "error", "", []byte{}, []byte{})
+	f.Add("token", "", "update", "info", "", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, urlSuffix, commitStatus, severity, message string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		opsgenie, err := NewOpsgenie(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", &cert, token)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+		event.Message = message
+
+		_ = opsgenie.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/rocket_test.go b/internal/notifier/rocket_test.go
index bfddcfc..b0de335 100644
--- a/internal/notifier/rocket_test.go
+++ b/internal/notifier/rocket_test.go
@@ -18,12 +18,16 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
 )
 
@@ -46,3 +50,39 @@ func TestRocket_Post(t *testing.T) {
 	err = rocket.Post(context.TODO(), testEvent())
 	require.NoError(t, err)
 }
+
+func Fuzz_Rocket(f *testing.F) {
+	f.Add("user", "channel", "", "error", "", "", []byte{}, []byte{})
+	f.Add("user", "channel", "", "error", "update", "", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		username, channel, urlSuffix, severity, commitStatus, message string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		rocket, err := NewRocket(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", &cert, username, channel)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+		event.Severity = severity
+		event.Message = message
+
+		_ = rocket.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go
index e9bcc5a..6b5b50e 100644
--- a/internal/notifier/slack_test.go
+++ b/internal/notifier/slack_test.go
@@ -18,12 +18,16 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
 )
 
@@ -56,3 +60,40 @@ func TestSlack_PostUpdate(t *testing.T) {
 	err = slack.Post(context.TODO(), event)
 	require.NoError(t, err)
 }
+
+func Fuzz_Slack(f *testing.F) {
+	f.Add("token", "user", "channel", "", "error", "", "", []byte{}, []byte{})
+	f.Add("token", "", "channel", "", "info", "", "", []byte{}, []byte{})
+	f.Add("token", "", "channel", "", "info", "update", "", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, username, channel, urlSuffix, severity, commitStatus, message string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		slack, err := NewSlack(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", token, &cert, username, channel)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+		event.Severity = severity
+		event.Message = message
+
+		_ = slack.Post(context.TODO(), event)
+	})
+}
diff --git a/internal/notifier/teams_test.go b/internal/notifier/teams_test.go
index 2d5111d..ccd5de1 100644
--- a/internal/notifier/teams_test.go
+++ b/internal/notifier/teams_test.go
@@ -18,12 +18,16 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
 )
 
@@ -46,3 +50,40 @@ func TestTeams_Post(t *testing.T) {
 	err = teams.Post(context.TODO(), testEvent())
 	require.NoError(t, err)
 }
+
+func Fuzz_MSTeams(f *testing.F) {
+	f.Add("token", "", "error", "", "", []byte{}, []byte{})
+	f.Add("token", "", "info", "", "", []byte{}, []byte{})
+	f.Add("token", "", "info", "", "update", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, urlSuffix, severity, message, commitStatus string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		teams, err := NewMSTeams(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", &cert)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+		event.Message = message
+		event.Severity = severity
+
+		_ = teams.Post(context.TODO(), event)
+	})
+}
diff --git a/tests/fuzz/util_fuzzer.go b/internal/notifier/telegram_test.go
similarity index 51%
rename from tests/fuzz/util_fuzzer.go
rename to internal/notifier/telegram_test.go
index 2552c0f..d5ccc8f 100644
--- a/tests/fuzz/util_fuzzer.go
+++ b/internal/notifier/telegram_test.go
@@ -1,8 +1,5 @@
-//go:build gofuzz
-// +build gofuzz
-
 /*
-Copyright 2021 The Flux authors
+Copyright 2022 The Flux authors
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,22 +17,13 @@ limitations under the License.
 package notifier
 
 import (
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
+	"testing"
 )
 
-// FuzzNotifierUtil implements a fuzzer that targets
-// notifier.formatNameAndDescription() and notifier.parseGitAddress().
-func FuzzNotifierUtil(data []byte) int {
-	f := fuzz.NewConsumer(data)
-	event := events.Event{}
-
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_, _ = formatNameAndDescription(event)
-	_, _, _ = parseGitAddress(string(data))
+func Fuzz_Telegram_escapeString(f *testing.F) {
+	f.Add("a-")
 
-	return 1
+	f.Fuzz(func(t *testing.T, str string) {
+		_ = escapeString(str)
+	})
 }
diff --git a/internal/notifier/util_test.go b/internal/notifier/util_test.go
index 397d0f8..b229f06 100644
--- a/internal/notifier/util_test.go
+++ b/internal/notifier/util_test.go
@@ -19,6 +19,7 @@ package notifier
 import (
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
 	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
 	v1 "k8s.io/api/core/v1"
@@ -123,3 +124,24 @@ func TestUtil_BasicAuth(t *testing.T) {
 	s := basicAuth(username, password)
 	require.Equal(t, "dXNlcjpwYXNzd29yZA==", s)
 }
+
+func Fuzz_Util_ParseGitAddress(f *testing.F) {
+	f.Add("ssh://git@abc.com")
+
+	f.Fuzz(func(t *testing.T, gitAddress string) {
+		_, _, _ = parseGitAddress(gitAddress)
+	})
+}
+
+func Fuzz_Util_FormatNameAndDescription(f *testing.F) {
+	f.Add("aA1-", []byte{})
+
+	f.Fuzz(func(t *testing.T, reason string, seed []byte) {
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		event.Reason = reason
+
+		_, _ = formatNameAndDescription(event)
+	})
+}
diff --git a/internal/notifier/webex_test.go b/internal/notifier/webex_test.go
index e8991e2..5b9428b 100644
--- a/internal/notifier/webex_test.go
+++ b/internal/notifier/webex_test.go
@@ -18,12 +18,16 @@ package notifier
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/stretchr/testify/require"
 )
 
@@ -54,3 +58,39 @@ func TestWebex_PostUpdate(t *testing.T) {
 	err = webex.Post(context.TODO(), event)
 	require.NoError(t, err)
 }
+
+func Fuzz_Webex(f *testing.F) {
+	f.Add("token", "channel", "", "error", "", "", []byte{}, []byte{})
+	f.Add("token", "channel", "", "info", "", "update", []byte{}, []byte{})
+
+	f.Fuzz(func(t *testing.T,
+		token, channel, urlSuffix, severity, message, commitStatus string, seed, response []byte) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write(response)
+			io.Copy(io.Discard, r.Body)
+			r.Body.Close()
+		}))
+		defer ts.Close()
+
+		var cert x509.CertPool
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
+
+		webex, err := NewWebex(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", &cert, channel, token)
+		if err != nil {
+			return
+		}
+
+		event := events.Event{}
+		_ = fuzz.NewConsumer(seed).GenerateStruct(&event)
+
+		if event.Metadata == nil {
+			event.Metadata = map[string]string{}
+		}
+
+		event.Metadata["commit_status"] = commitStatus
+		event.Message = message
+		event.Severity = severity
+
+		_ = webex.Post(context.TODO(), event)
+	})
+}
diff --git a/tests/fuzz/alertmanager_fuzzer.go b/tests/fuzz/alertmanager_fuzzer.go
deleted file mode 100644
index 81cc42e..0000000
--- a/tests/fuzz/alertmanager_fuzzer.go
+++ /dev/null
@@ -1,55 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzAlertmanager implements a fuzzer that targets Alertmanager.Post().
-func FuzzAlertmanager(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	alertmanager, err := NewAlertmanager(ts.URL, "", nil)
-	if err != nil {
-		return 0
-	}
-
-	f := fuzz.NewConsumer(data)
-	event := events.Event{}
-
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = alertmanager.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/azure_devops_fuzzer.go b/tests/fuzz/azure_devops_fuzzer.go
deleted file mode 100644
index 621e0bf..0000000
--- a/tests/fuzz/azure_devops_fuzzer.go
+++ /dev/null
@@ -1,59 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzAzureDevOps implements a fuzzer that targets AzureDevOps.Post().
-func FuzzAzureDevOps(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	f := fuzz.NewConsumer(data)
-	token, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	azureDevOps, err := NewAzureDevOps(ts.URL, token, nil)
-	if err != nil {
-		return 0
-	}
-
-	event := events.Event{}
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = azureDevOps.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/bitbucket_fuzzer.go b/tests/fuzz/bitbucket_fuzzer.go
deleted file mode 100644
index d0e8e1c..0000000
--- a/tests/fuzz/bitbucket_fuzzer.go
+++ /dev/null
@@ -1,59 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzBitbucket implements a fuzzer that targets Bitbucket.Post().
-func FuzzBitbucket(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	f := fuzz.NewConsumer(data)
-	token, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	bitbucket, err := NewBitbucket(ts.URL, token, nil)
-	if err != nil {
-		return 0
-	}
-
-	event := events.Event{}
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = bitbucket.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/discord_fuzzer.go b/tests/fuzz/discord_fuzzer.go
deleted file mode 100644
index 9cebf57..0000000
--- a/tests/fuzz/discord_fuzzer.go
+++ /dev/null
@@ -1,64 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzDiscord implements a fuzzer that targets Discord.Post().
-func FuzzDiscord(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	f := fuzz.NewConsumer(data)
-	username, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	channel, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	discord, err := NewDiscord(ts.URL, "", username, channel)
-	if err != nil {
-		return 0
-	}
-
-	event := events.Event{}
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = discord.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/forwarder_fuzzer.go b/tests/fuzz/forwarder_fuzzer.go
deleted file mode 100644
index b045fa3..0000000
--- a/tests/fuzz/forwarder_fuzzer.go
+++ /dev/null
@@ -1,56 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzForwarder implements a fuzzer that targets Forwarder.Post().
-func FuzzForwarder(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	header := make(map[string]string)
-	forwarder, err := NewForwarder(ts.URL, "", header, nil)
-	if err != nil {
-		return 0
-	}
-
-	f := fuzz.NewConsumer(data)
-	event := events.Event{}
-
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = forwarder.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/github_fuzzer.go b/tests/fuzz/github_fuzzer.go
deleted file mode 100644
index 56917c3..0000000
--- a/tests/fuzz/github_fuzzer.go
+++ /dev/null
@@ -1,59 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzGitHub implements a fuzzer that targets GitHub.Post().
-func FuzzGitHub(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	f := fuzz.NewConsumer(data)
-	token, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	github, err := NewGitHub(ts.URL, token, nil)
-	if err != nil {
-		return 0
-	}
-
-	event := events.Event{}
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = github.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/gitlab_fuzzer.go b/tests/fuzz/gitlab_fuzzer.go
deleted file mode 100644
index 03fc6eb..0000000
--- a/tests/fuzz/gitlab_fuzzer.go
+++ /dev/null
@@ -1,59 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzGitLab implements a fuzzer that targets GitLab.Post().
-func FuzzGitLab(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	f := fuzz.NewConsumer(data)
-	token, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	gitLab, err := NewGitLab(ts.URL, token, nil)
-	if err != nil {
-		return 0
-	}
-
-	event := events.Event{}
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = gitLab.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/go.mod b/tests/fuzz/go.mod
deleted file mode 100644
index a517047..0000000
--- a/tests/fuzz/go.mod
+++ /dev/null
@@ -1,11 +0,0 @@
-module github.com/fluxcd/notification-controller/tests/fuzz
-
-// This module is used only to avoid polluting the main module
-// with fuzz dependencies.
-
-go 1.17
-
-replace (
-	github.com/fluxcd/notification-controller/api => ../../api
-	github.com/fluxcd/notification-controller => ../../
-)
diff --git a/tests/fuzz/google_chat_fuzzer.go b/tests/fuzz/google_chat_fuzzer.go
deleted file mode 100644
index e4fc68e..0000000
--- a/tests/fuzz/google_chat_fuzzer.go
+++ /dev/null
@@ -1,55 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzGoogleChat implements a fuzzer that targets GoogleChat.Post().
-func FuzzGoogleChat(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	googlechat, err := NewGoogleChat(ts.URL, "")
-	if err != nil {
-		return 0
-	}
-
-	f := fuzz.NewConsumer(data)
-	event := events.Event{}
-
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = googlechat.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/lark_fuzzer.go b/tests/fuzz/lark_fuzzer.go
deleted file mode 100644
index 2e05549..0000000
--- a/tests/fuzz/lark_fuzzer.go
+++ /dev/null
@@ -1,55 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzLark implements a fuzzer that targets Lark.Post().
-func FuzzLark(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	lark, err := NewLark(ts.URL)
-	if err != nil {
-		return 0
-	}
-
-	f := fuzz.NewConsumer(data)
-	event := events.Event{}
-
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = lark.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/matrix_fuzzer.go b/tests/fuzz/matrix_fuzzer.go
deleted file mode 100644
index 4bcff2c..0000000
--- a/tests/fuzz/matrix_fuzzer.go
+++ /dev/null
@@ -1,59 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzMatrix implements a fuzzer that targets Matrix.Post().
-func FuzzMatrix(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	f := fuzz.NewConsumer(data)
-	token, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	matrix, err := NewMatrix(ts.URL, "", token, nil)
-	if err != nil {
-		return 0
-	}
-
-	event := events.Event{}
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = matrix.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/native_go_run.sh b/tests/fuzz/native_go_run.sh
new file mode 100755
index 0000000..afd9af6
--- /dev/null
+++ b/tests/fuzz/native_go_run.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+# Copyright 2022 The Flux authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -euxo pipefail
+
+# This script iterates through all go fuzzing targets, running each one
+# through the period of time established by FUZZ_TIME.
+
+FUZZ_TIME=${FUZZ_TIME:-"5s"}
+
+test_files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' .)
+
+for file in ${test_files}
+do
+	targets=$(grep -oP 'func \K(Fuzz\w*)' "${file}")
+	for target_name in ${targets}
+	do
+		echo "Running ${file}.${target_name} for ${FUZZ_TIME}."
+		file_dir=$(dirname "${file}")
+
+		go test -fuzz="^${target_name}\$" -fuzztime "${FUZZ_TIME}" "${file_dir}"
+	done
+done
diff --git a/tests/fuzz/opsgenie_fuzzer.go b/tests/fuzz/opsgenie_fuzzer.go
deleted file mode 100644
index db6c6c9..0000000
--- a/tests/fuzz/opsgenie_fuzzer.go
+++ /dev/null
@@ -1,55 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzOpsGenie implements a fuzzer that targets Opsgenie.Post().
-func FuzzOpsGenie(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	opsgenie, err := NewOpsgenie(ts.URL, "", nil, "token")
-	if err != nil {
-		return 0
-	}
-
-	f := fuzz.NewConsumer(data)
-	event := events.Event{}
-
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = opsgenie.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/oss_fuzz_build.sh b/tests/fuzz/oss_fuzz_build.sh
index 7712361..247d9b1 100755
--- a/tests/fuzz/oss_fuzz_build.sh
+++ b/tests/fuzz/oss_fuzz_build.sh
@@ -19,48 +19,61 @@ set -euxo pipefail
 GOPATH="${GOPATH:-/root/go}"
 GO_SRC="${GOPATH}/src"
 PROJECT_PATH="github.com/fluxcd/notification-controller"
+TMP_DIR=$(mktemp -d /tmp/oss_fuzz-XXXXXX)
 
-cd "${GO_SRC}"
-
-# Move fuzzer to their respective directories.
-# This removes dependency noises from the modules' go.mod and go.sum files.
-cp "${PROJECT_PATH}/tests/fuzz/util_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/alertmanager_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/opsgenie_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/webex_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/discord_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/forwarder_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/lark_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/matrix_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/rocket_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/slack_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/teams_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/google_chat_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/azure_devops_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/bitbucket_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/github_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-cp "${PROJECT_PATH}/tests/fuzz/gitlab_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
-
-
-# compile fuzz tests for the runtime module
-pushd "${PROJECT_PATH}"
-
-go get -d github.com/AdaLogics/go-fuzz-headers
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzNotifierUtil fuzz_notifier_util
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzAlertmanager fuzz_alert_manager
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzOpsGenie fuzz_opsgenie
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzWebex fuzz_webex
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzDiscord fuzz_discord
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzForwarder fuzz_forwarder
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzLark fuzz_lark
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzMatrix fuzz_matrix
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzRocket fuzz_rocket
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzSlack fuzz_slack
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzMSTeams fuzz_msteams
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzGoogleChat fuzz_google_chat
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzAzureDevOps fuzz_azure_devops
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzBitbucket fuzz_bitbucket
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzGitHub fuzz_github
-compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzGitLab fuzz_gitlab
-
-popd
+cleanup(){
+	rm -rf "${TMP_DIR}"
+}
+trap cleanup EXIT
+
+install_deps(){
+	if ! command -v go-118-fuzz-build &> /dev/null || ! command -v addimport &> /dev/null; then
+		mkdir -p "${TMP_DIR}/go-118-fuzz-build"
+
+		git clone https://github.com/AdamKorcz/go-118-fuzz-build "${TMP_DIR}/go-118-fuzz-build"
+		cd "${TMP_DIR}/go-118-fuzz-build"
+		go build -o "${GOPATH}/bin/go-118-fuzz-build"
+
+		cd addimport
+		go build -o "${GOPATH}/bin/addimport"
+	fi
+
+	if ! command -v goimports &> /dev/null; then
+		go install golang.org/x/tools/cmd/goimports@latest
+	fi
+}
+
+# Removes the content of test funcs which could cause the Fuzz
+# tests to break.
+remove_test_funcs(){
+	filename=$1
+
+	echo "removing co-located *testing.T"
+	sed -i -e '/func Test.*testing.T) {$/ {:r;/\n}/!{N;br}; s/\n.*\n/\n/}' "${filename}"
+
+	# After removing the body of the go testing funcs, consolidate the imports.
+	goimports -w "${filename}"
+}
+
+install_deps
+
+cd "${GO_SRC}/${PROJECT_PATH}"
+
+go get github.com/AdamKorcz/go-118-fuzz-build/utils
+
+# Iterate through all Go Fuzz targets, compiling each into a fuzzer.
+test_files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' .)
+for file in ${test_files}
+do
+	remove_test_funcs "${file}"
+
+	targets=$(grep -oP 'func \K(Fuzz\w*)' "${file}")
+	for target_name in ${targets}
+	do
+		fuzzer_name=$(echo "${target_name}" | tr '[:upper:]' '[:lower:]')
+		target_dir=$(dirname "${file}")
+
+		echo "Building ${file}.${target_name} into ${fuzzer_name}"
+		compile_native_go_fuzzer "${target_dir}" "${target_name}" "${fuzzer_name}"
+	done
+done
diff --git a/tests/fuzz/rocket_fuzzer.go b/tests/fuzz/rocket_fuzzer.go
deleted file mode 100644
index 8035f99..0000000
--- a/tests/fuzz/rocket_fuzzer.go
+++ /dev/null
@@ -1,64 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzRocket implements a fuzzer that targets Rocket.Post().
-func FuzzRocket(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	f := fuzz.NewConsumer(data)
-	username, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	channel, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	rocket, err := NewRocket(ts.URL, "", nil, username, channel)
-	if err != nil {
-		return 0
-	}
-
-	event := events.Event{}
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = rocket.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/slack_fuzzer.go b/tests/fuzz/slack_fuzzer.go
deleted file mode 100644
index a165b29..0000000
--- a/tests/fuzz/slack_fuzzer.go
+++ /dev/null
@@ -1,69 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzSlack implements a fuzzer that targets Slack.Post().
-func FuzzSlack(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	f := fuzz.NewConsumer(data)
-	username, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	channel, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	token, err := f.GetString()
-	if err != nil {
-		return 0
-	}
-
-	slack, err := NewSlack(ts.URL, "", token, nil, username, channel)
-	if err != nil {
-		return 0
-	}
-
-	event := events.Event{}
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = slack.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/teams_fuzzer.go b/tests/fuzz/teams_fuzzer.go
deleted file mode 100644
index 4e90e25..0000000
--- a/tests/fuzz/teams_fuzzer.go
+++ /dev/null
@@ -1,55 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzMSTeams implements a fuzzer that targets MSTeams.Post().
-func FuzzMSTeams(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	teams, err := NewMSTeams(ts.URL, "", nil)
-	if err != nil {
-		return 0
-	}
-
-	f := fuzz.NewConsumer(data)
-	event := events.Event{}
-
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = teams.Post(context.TODO(), event)
-
-	return 1
-}
diff --git a/tests/fuzz/webex_fuzzer.go b/tests/fuzz/webex_fuzzer.go
deleted file mode 100644
index 6fbbde1..0000000
--- a/tests/fuzz/webex_fuzzer.go
+++ /dev/null
@@ -1,55 +0,0 @@
-//go:build gofuzz
-// +build gofuzz
-
-/*
-Copyright 2022 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package notifier
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"net/http/httptest"
-
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-	"github.com/fluxcd/pkg/runtime/events"
-)
-
-// FuzzWebex implements a fuzzer that targets Webex.Post().
-func FuzzWebex(data []byte) int {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		io.ReadAll(r.Body)
-		r.Body.Close()
-	}))
-	defer ts.Close()
-
-	webex, err := NewWebex(ts.URL, "", nil, "", "")
-	if err != nil {
-		return 0
-	}
-
-	f := fuzz.NewConsumer(data)
-	event := events.Event{}
-
-	if err := f.GenerateStruct(&event); err != nil {
-		return 0
-	}
-
-	_ = webex.Post(context.TODO(), event)
-
-	return 1
-}