Skip to content
Merged
Prev Previous commit
Next Next commit
Rework sdk
- have the hook request implement the methods in a more straight forward
  way
- pass in ctx into the hook function to keep best practices and ensure
  the funcs remain cancellable
- include a test helper in the sdk to keep hook boilerplate out of the
  hooks

Signed-off-by: Jannik Hollenbach <jannik.hollenbach@iteratec.com>
  • Loading branch information
J12934 committed Sep 9, 2026
commit 484e15fb64fbf95a7022cc95ea946a70582c835d
52 changes: 52 additions & 0 deletions hook-sdk/golang/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0

package hooksdk

import "context"

// HookRequestMock is a configurable HookRequest implementation for hook tests.
// Unconfigured methods return their zero values without an error.
type HookRequestMock struct {
ScanFunc func(context.Context) (*Scan, error)
GetRawResultsFunc func(context.Context) (string, error)
GetFindingsFunc func(context.Context) ([]Finding, error)
UpdateRawResultsFunc func(context.Context, string) error
UpdateFindingsFunc func(context.Context, []Finding) error
}

func (m *HookRequestMock) Scan(ctx context.Context) (*Scan, error) {
if m.ScanFunc != nil {
return m.ScanFunc(ctx)
}
return nil, nil
}

func (m *HookRequestMock) GetRawResults(ctx context.Context) (string, error) {
if m.GetRawResultsFunc != nil {
return m.GetRawResultsFunc(ctx)
}
return "", nil
}

func (m *HookRequestMock) GetFindings(ctx context.Context) ([]Finding, error) {
if m.GetFindingsFunc != nil {
return m.GetFindingsFunc(ctx)
}
return nil, nil
}

func (m *HookRequestMock) UpdateRawResults(ctx context.Context, content string) error {
if m.UpdateRawResultsFunc != nil {
return m.UpdateRawResultsFunc(ctx, content)
}
return nil
}

func (m *HookRequestMock) UpdateFindings(ctx context.Context, findings []Finding) error {
if m.UpdateFindingsFunc != nil {
return m.UpdateFindingsFunc(ctx, findings)
}
return nil
}
160 changes: 80 additions & 80 deletions hook-sdk/golang/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,90 @@ type HookHandler interface {
}

type HookRequest interface {
Scan() *Scan
GetRawResults() (string, error)
GetFindings() ([]Finding, error)
UpdateRawResults(content string) error
UpdateFindings(findings []Finding) error
Scan(ctx context.Context) (*Scan, error)
GetRawResults(ctx context.Context) (string, error)
GetFindings(ctx context.Context) ([]Finding, error)
UpdateRawResults(ctx context.Context, content string) error
UpdateFindings(ctx context.Context, findings []Finding) error
}

type hookRequest struct {
scan *Scan
getRawResults func() (string, error)
getFindings func() ([]Finding, error)
updateRawResults func(string) error
updateFindings func([]Finding) error
k8sClient K8sClient
fileClient FileClient
scanName string
namespace string
urls []string
}

func (r *hookRequest) Scan(ctx context.Context) (*Scan, error) {
scan, err := r.k8sClient.GetScan(ctx, r.scanName, r.namespace)
if err != nil {
return nil, fmt.Errorf("get Scan from Kubernetes API: %w", err)
}
return scan, nil
}

func (r *hookRequest) GetRawResults(ctx context.Context) (string, error) {
url := r.urlAt(0)
if url == "" {
return "", fmt.Errorf("raw results download URL not provided")
}
return r.fileClient.DownloadText(ctx, url)
}

func (r *hookRequest) GetFindings(ctx context.Context) ([]Finding, error) {
url := r.urlAt(1)
if url == "" {
return nil, fmt.Errorf("findings download URL not provided")
}
var findings []Finding
if err := r.fileClient.DownloadJSON(ctx, url, &findings); err != nil {
return nil, fmt.Errorf("download findings: %w", err)
}
for index, finding := range findings {
if err := ValidateFinding(finding, index); err != nil {
return nil, err
}
}
return findings, nil
}

func (r *hookRequest) Scan() *Scan { return r.scan }
func (r *hookRequest) GetRawResults() (string, error) { return r.getRawResults() }
func (r *hookRequest) GetFindings() ([]Finding, error) { return r.getFindings() }
func (r *hookRequest) UpdateRawResults(content string) error { return r.updateRawResults(content) }
func (r *hookRequest) UpdateFindings(findings []Finding) error {
return r.updateFindings(findings)
func (r *hookRequest) UpdateRawResults(ctx context.Context, content string) error {
url := r.urlAt(2)
if url == "" {
return fmt.Errorf("cannot update raw results in a ReadOnly hook")
}
return r.fileClient.Upload(ctx, url, "", []byte(content))
}

func (r *hookRequest) UpdateFindings(ctx context.Context, findings []Finding) error {
url := r.urlAt(3)
if url == "" {
return fmt.Errorf("cannot update findings in a ReadOnly hook")
}
for index, finding := range findings {
if err := ValidateFinding(finding, index); err != nil {
return err
}
}
body, err := json.Marshal(findings)
if err != nil {
return fmt.Errorf("marshal findings: %w", err)
}
if err := r.fileClient.Upload(ctx, url, "", body); err != nil {
return fmt.Errorf("upload findings: %w", err)
}
if err := r.k8sClient.PatchScanStatus(ctx, r.scanName, r.namespace, findings); err != nil {
return fmt.Errorf("update scan status: %w", err)
}
return nil
}

func (r *hookRequest) urlAt(index int) string {
if index < len(r.urls) {
return r.urls[index]
}
return ""
}

type Client struct {
Expand Down Expand Up @@ -85,70 +148,7 @@ func NewClient(opts ...Option) (*Client, error) {
}

func (c *Client) Run(ctx context.Context, handler HookHandler) error {
Comment thread
J12934 marked this conversation as resolved.
scan, err := c.k8sClient.GetScan(ctx, c.scanName, c.namespace)
if err != nil {
return fmt.Errorf("get Scan from Kubernetes API: %w", err)
}
urls := c.args[1:]
urlAt := func(index int) string {
if index < len(urls) {
return urls[index]
}
return ""
}
getRawResults := func() (string, error) {
url := urlAt(0)
if url == "" {
return "", fmt.Errorf("raw results download URL not provided")
}
return c.fileClient.DownloadText(ctx, url)
}
getFindings := func() ([]Finding, error) {
url := urlAt(1)
if url == "" {
return nil, fmt.Errorf("findings download URL not provided")
}
var findings []Finding
if err := c.fileClient.DownloadJSON(ctx, url, &findings); err != nil {
return nil, fmt.Errorf("download findings: %w", err)
}
for index, finding := range findings {
if err := ValidateFinding(finding, index); err != nil {
return nil, err
}
}
return findings, nil
}
updateRawResults := func(content string) error {
url := urlAt(2)
if url == "" {
return fmt.Errorf("cannot update raw results in a ReadOnly hook")
}
return c.fileClient.Upload(ctx, url, "", []byte(content))
}
updateFindings := func(findings []Finding) error {
url := urlAt(3)
if url == "" {
return fmt.Errorf("cannot update findings in a ReadOnly hook")
}
for index, finding := range findings {
if err := ValidateFinding(finding, index); err != nil {
return err
}
}
body, err := json.Marshal(findings)
if err != nil {
return fmt.Errorf("marshal findings: %w", err)
}
if err := c.fileClient.Upload(ctx, url, "", body); err != nil {
return fmt.Errorf("upload findings: %w", err)
}
if err := c.k8sClient.PatchScanStatus(ctx, c.scanName, c.namespace, findings); err != nil {
return fmt.Errorf("update scan status: %w", err)
}
return nil
}
request := &hookRequest{scan: scan, getRawResults: getRawResults, getFindings: getFindings, updateRawResults: updateRawResults, updateFindings: updateFindings}
request := &hookRequest{k8sClient: c.k8sClient, fileClient: c.fileClient, scanName: c.scanName, namespace: c.namespace, urls: c.args[1:]}
if err := handler.Handle(ctx, request); err != nil {
return fmt.Errorf("run hook handler: %w", err)
}
Expand Down
95 changes: 95 additions & 0 deletions hook-sdk/golang/sdk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0

package hooksdk

import (
"context"
"encoding/json"
"testing"
)

type contextKey struct{}

type stubK8sClient struct {
scanCtx context.Context
patchCtx context.Context
}

func (s *stubK8sClient) GetScan(ctx context.Context, _, _ string) (*Scan, error) {
s.scanCtx = ctx
return &Scan{Name: "scan"}, nil
}

func (s *stubK8sClient) PatchScanStatus(ctx context.Context, _, _ string, _ []Finding) error {
s.patchCtx = ctx
return nil
}

type stubFileClient struct {
downloadTextCtx context.Context
downloadJSONCtx context.Context
uploadCtx context.Context
}

func (s *stubFileClient) DownloadText(ctx context.Context, _ string) (string, error) {
s.downloadTextCtx = ctx
return "raw results", nil
}

func (s *stubFileClient) DownloadJSON(ctx context.Context, _ string, value any) error {
s.downloadJSONCtx = ctx
data, err := json.Marshal([]Finding{validFinding()})
if err != nil {
return err
}
return json.Unmarshal(data, value)
}

func (s *stubFileClient) Upload(ctx context.Context, _ string, _ string, _ []byte) error {
s.uploadCtx = ctx
return nil
}

func TestHookRequestDefersOperationsAndPassesContext(t *testing.T) {
k8sClient := &stubK8sClient{}
fileClient := &stubFileClient{}
request := &hookRequest{
k8sClient: k8sClient,
fileClient: fileClient,
scanName: "scan",
namespace: "default",
urls: []string{"raw", "findings", "raw-upload", "findings-upload"},
}
ctx := context.WithValue(context.Background(), contextKey{}, "hook context")

if _, err := request.Scan(ctx); err != nil {
t.Fatal(err)
}
if _, err := request.GetRawResults(ctx); err != nil {
t.Fatal(err)
}
findings, err := request.GetFindings(ctx)
if err != nil {
t.Fatal(err)
}
if err := request.UpdateRawResults(ctx, "updated raw results"); err != nil {
t.Fatal(err)
}
if err := request.UpdateFindings(ctx, findings); err != nil {
t.Fatal(err)
}

for name, actual := range map[string]context.Context{
"GetScan": k8sClient.scanCtx,
"DownloadText": fileClient.downloadTextCtx,
"DownloadJSON": fileClient.downloadJSONCtx,
"Upload": fileClient.uploadCtx,
"PatchScanStatus": k8sClient.patchCtx,
} {
if actual != ctx {
t.Errorf("%s received a different context", name)
}
}
}
6 changes: 3 additions & 3 deletions hooks/finding-post-processing/hook/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ func newHandler(rulesJSON string) (*handler, error) {
return &handler{rules: rules}, nil
}

func (h *handler) Handle(_ context.Context, request hooksdk.HookRequest) error {
findings, err := request.GetFindings()
func (h *handler) Handle(ctx context.Context, request hooksdk.HookRequest) error {
findings, err := request.GetFindings(ctx)
if err != nil {
return err
}
Expand All @@ -83,7 +83,7 @@ func (h *handler) Handle(_ context.Context, request hooksdk.HookRequest) error {
}
}
if matched {
return request.UpdateFindings(findings)
return request.UpdateFindings(ctx, findings)
}
return nil
}
Expand Down
Loading