Skip to content

Commit 96a010b

Browse files
authoredApr 28, 2024··
feat: Add custom header options for webhook notification (#2044)
* feat: Add custom header options for webhook notification * feat: Add `webhookBroadcastCustomHeaders` helm deploy values * chore: Add `operator.webhookBroadcastCustomHeaders` option to helm README.md * chore: mage generated helm README.md
1 parent dca19ab commit 96a010b

File tree

10 files changed

+115
-42
lines changed

10 files changed

+115
-42
lines changed
 

‎deploy/helm/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Keeps security report resources updated
8383
| operator.valuesFromSecret | string | `""` | valuesFromSecret name of a Secret to apply OPERATOR_* environment variables. Will override Helm AND ConfigMap values. |
8484
| operator.vulnerabilityScannerEnabled | bool | `true` | the flag to enable vulnerability scanner |
8585
| operator.vulnerabilityScannerScanOnlyCurrentRevisions | bool | `true` | vulnerabilityScannerScanOnlyCurrentRevisions the flag to only create vulnerability scans on the current revision of a deployment. |
86+
| operator.webhookBroadcastCustomHeaders | string | `""` | webhookBroadcastCustomHeaders the flag to set webhook endpoint sent with custom defined headers if webhookBroadcastURL is enabled |
8687
| operator.webhookBroadcastTimeout | string | `"30s"` | webhookBroadcastTimeout the flag to set timeout for webhook requests if webhookBroadcastURL is enabled |
8788
| operator.webhookBroadcastURL | string | `""` | webhookBroadcastURL the flag to set reports should be sent to a webhook endpoint. "" means that the webhookBroadcastURL feature is disabled |
8889
| operator.webhookSendDeletedReports | bool | `false` | webhookSendDeletedReports the flag to enable sending deleted reports if webhookBroadcastURL is enabled |

‎deploy/helm/templates/configmaps/trivy-operator-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ data:
3737
OPERATOR_METRICS_CLUSTER_COMPLIANCE_INFO_ENABLED: {{ .Values.operator.metricsClusterComplianceInfo | quote }}
3838
OPERATOR_WEBHOOK_BROADCAST_URL: {{ .Values.operator.webhookBroadcastURL | quote }}
3939
OPERATOR_WEBHOOK_BROADCAST_TIMEOUT: {{ .Values.operator.webhookBroadcastTimeout | quote }}
40+
OPERATOR_WEBHOOK_BROADCAST_CUSTOM_HEADERS: {{ .Values.operator.webhookBroadcastCustomHeaders | quote }}
4041
OPERATOR_SEND_DELETED_REPORTS: {{ .Values.operator.webhookSendDeletedReports | quote }}
4142
OPERATOR_PRIVATE_REGISTRY_SCAN_SECRETS_NAMES: {{ .Values.operator.privateRegistryScanSecretsNames | toJson | quote }}
4243
OPERATOR_ACCESS_GLOBAL_SECRETS_SERVICE_ACCOUNTS: {{ .Values.operator.accessGlobalSecretsAndServiceAccount | quote }}

‎deploy/helm/values.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ operator:
157157
# -- webhookBroadcastTimeout the flag to set timeout for webhook requests if webhookBroadcastURL is enabled
158158
webhookBroadcastTimeout: 30s
159159

160+
# -- webhookBroadcastCustomHeaders the flag to set webhook endpoint sent with custom defined headers if webhookBroadcastURL is enabled
161+
webhookBroadcastCustomHeaders: ""
162+
160163
# -- webhookSendDeletedReports the flag to enable sending deleted reports if webhookBroadcastURL is enabled
161164
webhookSendDeletedReports: false
162165

‎deploy/static/trivy-operator.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -3003,6 +3003,7 @@ data:
30033003
OPERATOR_METRICS_CLUSTER_COMPLIANCE_INFO_ENABLED: "false"
30043004
OPERATOR_WEBHOOK_BROADCAST_URL: ""
30053005
OPERATOR_WEBHOOK_BROADCAST_TIMEOUT: "30s"
3006+
OPERATOR_WEBHOOK_BROADCAST_CUSTOM_HEADERS: ""
30063007
OPERATOR_SEND_DELETED_REPORTS: "false"
30073008
OPERATOR_PRIVATE_REGISTRY_SCAN_SECRETS_NAMES: "{}"
30083009
OPERATOR_ACCESS_GLOBAL_SECRETS_SERVICE_ACCOUNTS: "true"

‎docs/getting-started/installation/configuration.md

+36-35
Large diffs are not rendered by default.

‎docs/tutorials/integrations/webhook.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ You can enable the Webhook integration as follows:
1111
1. Required: Set `OPERATOR_WEBHOOK_BROADCAST_URL` to the webhook endpoint you'd like to send the reports to.
1212
2. Optional: Set `OPERATOR_WEBHOOK_BROADCAST_TIMEOUT` to a time limit that suites your use case. Default is `30s`.
1313
3. Optional: Set `OPERATOR_SEND_DELETED_REPORTS` to `true` to send webhook notifications when reports are deleted. Default is `false`.
14+
4. Optional: Set `OPERATOR_WEBHOOK_BROADCAST_CUSTOM_HEADERS` to comma seperated `key:value` to send webhook notifications with custom headers. Default is ``
1415

1516
The Webhook integration support the following reports types:
1617

‎pkg/operator/etc/config.go

+19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package etc
33
import (
44
"encoding/json"
55
"fmt"
6+
"net/http"
67
"strings"
78
"time"
89

@@ -51,6 +52,7 @@ type Config struct {
5152
ExposedSecretScannerEnabled bool `env:"OPERATOR_EXPOSED_SECRET_SCANNER_ENABLED" envDefault:"true"`
5253
WebhookBroadcastURL string `env:"OPERATOR_WEBHOOK_BROADCAST_URL"`
5354
WebhookBroadcastTimeout *time.Duration `env:"OPERATOR_WEBHOOK_BROADCAST_TIMEOUT" envDefault:"30s"`
55+
WebhookBroadcastCustomHeaders string `env:"OPERATOR_WEBHOOK_BROADCAST_CUSTOM_HEADERS"`
5456
WebhookSendDeletedReports bool `env:"OPERATOR_SEND_DELETED_REPORTS" envDefault:"false"`
5557
TargetWorkloads string `env:"OPERATOR_TARGET_WORKLOADS" envDefault:"Pod,ReplicaSet,ReplicationController,StatefulSet,DaemonSet,CronJob,Job"`
5658
AccessGlobalSecretsAndServiceAccount bool `env:"OPERATOR_ACCESS_GLOBAL_SECRETS_SERVICE_ACCOUNTS" envDefault:"true"`
@@ -103,6 +105,23 @@ func (c Config) GetPrivateRegistryScanSecretsNames() (map[string]string, error)
103105
return secretsInfoMap, nil
104106
}
105107

108+
func (c Config) GetWebhookBroadcastCustomHeaders() http.Header {
109+
customHeaders := c.WebhookBroadcastCustomHeaders
110+
headers := http.Header{}
111+
112+
if customHeaders != "" {
113+
for _, header := range strings.Split(customHeaders, ",") {
114+
s := strings.SplitN(header, ":", 2)
115+
if len(s) != 2 {
116+
continue
117+
}
118+
headers.Set(s[0], s[1])
119+
}
120+
}
121+
122+
return headers
123+
}
124+
106125
func (c Config) GetTargetWorkloads() []string {
107126
workloads := c.TargetWorkloads
108127
if workloads != "" {

‎pkg/operator/etc/config_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package etc_test
22

33
import (
4+
"net/http"
45
"testing"
56

67
"github.com/aquasecurity/trivy-operator/pkg/operator/etc"
@@ -104,6 +105,35 @@ func TestOperator_ResolveInstallMode(t *testing.T) {
104105
}
105106
}
106107

108+
func TestOperator_GetWebhookBroadcastCustomHeaders(t *testing.T) {
109+
testCases := []struct {
110+
name string
111+
operator etc.Config
112+
expectedWebhookBroadcastCustomHeaders http.Header
113+
}{
114+
{
115+
name: "Should return single custom header",
116+
operator: etc.Config{
117+
WebhookBroadcastCustomHeaders: "x-api-key:trivy",
118+
},
119+
expectedWebhookBroadcastCustomHeaders: http.Header{"X-Api-Key": {"trivy"}},
120+
},
121+
{
122+
name: "Should return multiple custom headers",
123+
operator: etc.Config{
124+
WebhookBroadcastCustomHeaders: "x-api-key:trivy,X-Api-User:trivy-operator,X-API-TOKEN:trivy-token",
125+
},
126+
expectedWebhookBroadcastCustomHeaders: http.Header{"X-Api-Key": {"trivy"}, "X-Api-User": {"trivy-operator"}, "X-Api-Token": {"trivy-token"}},
127+
},
128+
}
129+
130+
for _, tc := range testCases {
131+
t.Run(tc.name, func(t *testing.T) {
132+
assert.Equal(t, tc.expectedWebhookBroadcastCustomHeaders, tc.operator.GetWebhookBroadcastCustomHeaders())
133+
})
134+
}
135+
}
136+
107137
func TestOperator_GetTargetWorkloads(t *testing.T) {
108138
testCases := []struct {
109139
name string

‎pkg/webhook/webhookreporter.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,35 @@ func (r *WebhookReconciler) reconcileReport(reportType client.Object) reconcile.
9191
return ctrl.Result{}, nil
9292
}
9393

94+
webhookBroadcastCustomHeaders := r.Config.GetWebhookBroadcastCustomHeaders()
95+
9496
if r.WebhookSendDeletedReports {
9597
msg := WebhookMsg{OperatorObject: reportType, Verb: verb}
9698

97-
return ctrl.Result{}, sendReport(msg, r.WebhookBroadcastURL, *r.WebhookBroadcastTimeout)
99+
return ctrl.Result{}, sendReport(msg, r.WebhookBroadcastURL, *r.WebhookBroadcastTimeout, webhookBroadcastCustomHeaders)
98100
}
99-
return ctrl.Result{}, sendReport(reportType, r.WebhookBroadcastURL, *r.WebhookBroadcastTimeout)
101+
return ctrl.Result{}, sendReport(reportType, r.WebhookBroadcastURL, *r.WebhookBroadcastTimeout, webhookBroadcastCustomHeaders)
100102
}
101103
}
102104

103-
func sendReport[T any](reports T, endpoint string, timeout time.Duration) error {
105+
func sendReport[T any](reports T, endpoint string, timeout time.Duration, headerValues http.Header) error {
104106
b, err := json.Marshal(reports)
105107
if err != nil {
106108
return fmt.Errorf("failed to marshal reports: %w", err)
107109
}
108110
hc := http.Client{
109111
Timeout: timeout,
110112
}
111-
_, err = hc.Post(endpoint, "application/json", bytes.NewBuffer(b))
113+
114+
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(b))
115+
if err != nil {
116+
return fmt.Errorf("failed to make a new request: %w", err)
117+
}
118+
119+
headerValues.Set("Content-Type", "application/json")
120+
req.Header = headerValues
121+
122+
_, err = hc.Do(req)
112123
if err != nil {
113124
return fmt.Errorf("failed to send reports to endpoint: %w", err)
114125
}

‎pkg/webhook/webhookreporter_test.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func Test_sendReports(t *testing.T) {
2020
inputReport any
2121
timeout time.Duration
2222
expectedError string
23+
headerValues http.Header
2324
}{
2425
{
2526
name: "happy path, vuln report data",
@@ -38,7 +39,8 @@ func Test_sendReports(t *testing.T) {
3839
},
3940
},
4041
},
41-
timeout: time.Hour,
42+
timeout: time.Hour,
43+
headerValues: http.Header{},
4244
},
4345
{
4446
name: "happy path, secret report data",
@@ -55,7 +57,8 @@ func Test_sendReports(t *testing.T) {
5557
},
5658
},
5759
},
58-
timeout: time.Hour,
60+
timeout: time.Hour,
61+
headerValues: http.Header{},
5962
},
6063
{
6164
name: "sad path, timeout occurs",
@@ -64,12 +67,14 @@ func Test_sendReports(t *testing.T) {
6467
},
6568
timeout: time.Nanosecond,
6669
expectedError: "context deadline exceeded (Client.Timeout exceeded while awaiting headers)",
70+
headerValues: http.Header{},
6771
},
6872
{
6973
name: "sad path, bad report",
7074
inputReport: math.Inf(1),
7175
timeout: time.Hour,
7276
expectedError: `failed to marshal reports`,
77+
headerValues: http.Header{},
7378
},
7479
}
7580

@@ -80,7 +85,7 @@ func Test_sendReports(t *testing.T) {
8085
assert.JSONEq(t, tc.want, string(b))
8186
}))
8287
defer ts.Close()
83-
gotError := sendReport(tc.inputReport, ts.URL, tc.timeout)
88+
gotError := sendReport(tc.inputReport, ts.URL, tc.timeout, tc.headerValues)
8489
switch {
8590
case tc.expectedError != "":
8691
assert.ErrorContains(t, gotError, tc.expectedError, tc.name)

0 commit comments

Comments
 (0)
Please sign in to comment.