Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/controlplane/plugins/core/slack-webhook/v1/slack_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,18 @@ type registrationRequest struct {
WebhookURL string `json:"webhook" jsonschema:"format=uri,description=URL of the slack webhook"`
}

// registrationState defines the state stored after registration
type registrationState struct {
WebhookURL string `json:"webhook,omitempty"`
}

type attachmentRequest struct{}

func New(l log.Logger) (sdk.FanOut, error) {
base, err := sdk.NewFanOut(
&sdk.NewParams{
ID: "slack-webhook",
Version: "1.0",
Version: "1.1",
Description: "Send attestations to Slack",
Logger: l,
InputSchema: &sdk.InputSchema{
Expand Down Expand Up @@ -75,7 +80,14 @@ func (i *Integration) Register(_ context.Context, req *sdk.RegistrationRequest)
return nil, fmt.Errorf("error validating a webhook: %w", err)
}

// Store a masked version of the URL as non-secret config so it can be displayed for identification
config, err := sdk.ToConfig(&registrationState{WebhookURL: sdk.MaskURL(request.WebhookURL)})
if err != nil {
return nil, fmt.Errorf("marshalling configuration: %w", err)
}

return &sdk.RegistrationResponse{
Configuration: config,
// We treat the webhook URL as a sensitive field so we store it in the credentials storage
Credentials: &sdk.Credentials{Password: request.WebhookURL},
}, nil
Expand Down
8 changes: 4 additions & 4 deletions app/controlplane/plugins/core/webhook/v1/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ type attachmentState struct {

// registrationState defines the state stored after registration
type registrationState struct {
// No additional state needed for webhook besides the URL stored in credentials
WebhookURL string `json:"url,omitempty"`
}

// webhookPayload defines the JSON schema for the webhook payload
Expand All @@ -69,7 +69,7 @@ func New(l log.Logger) (sdk.FanOut, error) {
base, err := sdk.NewFanOut(
&sdk.NewParams{
ID: "webhook",
Version: "1.0",
Version: "1.1",
Description: "Send Attestation and SBOMs to a generic POST webhook URL",
Logger: l,
InputSchema: &sdk.InputSchema{
Expand Down Expand Up @@ -119,8 +119,8 @@ func (i *Integration) Register(ctx context.Context, req *sdk.RegistrationRequest
URL: regReq.URL, // Storing the URL in the URL field
}

// No additional state needed
rawConfig, err := sdk.ToConfig(&registrationState{})
// Store a masked version of the URL as non-secret config so it can be displayed for identification
rawConfig, err := sdk.ToConfig(&registrationState{WebhookURL: sdk.MaskURL(regReq.URL)})
if err != nil {
i.Logger.Errorw("failed to marshal registration state", "error", err)
return nil, fmt.Errorf("marshalling configuration: %w", err)
Expand Down
19 changes: 19 additions & 0 deletions app/controlplane/plugins/sdk/v1/integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"sort"

"github.com/invopop/jsonschema"
Expand Down Expand Up @@ -236,6 +237,24 @@ func FromConfig(data Configuration, v any) error {
return json.Unmarshal(data, v)
}

// MaskURL returns a masked version of the URL suitable for display.
// It preserves the scheme and host, and shows only the last 4 characters
// of the path with the rest replaced by asterisks.
func MaskURL(raw string) string {
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return ""
}

path := u.Path
const suffixLen = 4
if len(path) > suffixLen {
path = "****" + path[len(path)-suffixLen:]
}

return u.Scheme + "://" + u.Host + path
}

// GenerateJSONSchema generates a flat JSON schema from a struct using https://github.com/invopop/jsonschema
// We've put some limitations on the kind of input structs we support, for example:
// - Nested schemas are not supported
Expand Down
78 changes: 78 additions & 0 deletions app/controlplane/plugins/sdk/v1/integrations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// Copyright 2026 The Chainloop 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 sdk

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestMaskURL(t *testing.T) {
tests := []struct {
name string
raw string
want string
}{
{
name: "long path is masked with last 4 visible",
raw: "https://example.com/some/long/path/with/many/segments/secret-token",
want: "https://example.com****oken",
},
{
name: "short path is not masked",
raw: "https://example.com/sho",
want: "https://example.com/sho",
},
{
name: "path at threshold is not masked",
raw: "https://example.com/abc",
want: "https://example.com/abc",
},
{
name: "path above threshold is masked",
raw: "https://example.com/abcde",
want: "https://example.com****bcde",
},
{
name: "empty string returns empty",
raw: "",
want: "",
},
{
name: "missing host returns empty",
raw: "/just/a/path",
want: "",
},
{
name: "webhook URL with port",
raw: "https://prod-00.westus.logic.azure.com:443/workflows/1234567890abcdef/triggers/manual/paths/invoke",
want: "https://prod-00.westus.logic.azure.com:443****voke",
},
{
name: "no path",
raw: "https://example.com",
want: "https://example.com",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MaskURL(tt.raw)
assert.Equal(t, tt.want, got)
})
}
}
4 changes: 2 additions & 2 deletions devel/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ Below you can find the list of currently available integrations. If you can't fi
| [dependency-track](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/dependency-track/v1/README.md) | 1.7 | Send CycloneDX SBOMs to your Dependency-Track instance | SBOM_CYCLONEDX_JSON |
| [discord-webhook](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/discord-webhook/v1/README.md) | 1.1 | Send attestations to Discord | |
| [guac](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/guac/v1/README.md) | 1.0 | Export Attestation and SBOMs metadata to a blob storage backend so guacsec/guac can consume it | SBOM_CYCLONEDX_JSON, SBOM_SPDX_JSON |
| [slack-webhook](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/slack-webhook/v1/README.md) | 1.0 | Send attestations to Slack | |
| [slack-webhook](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/slack-webhook/v1/README.md) | 1.1 | Send attestations to Slack | |
| [smtp](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/smtp/v1/README.md) | 1.0 | Send emails with information about a received attestation | |
| [webhook](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/webhook/v1/README.md) | 1.0 | Send Attestation and SBOMs to a generic POST webhook URL | SBOM_CYCLONEDX_JSON, SBOM_SPDX_JSON |
| [webhook](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/webhook/v1/README.md) | 1.1 | Send Attestation and SBOMs to a generic POST webhook URL | SBOM_CYCLONEDX_JSON, SBOM_SPDX_JSON |

## How to use integrations

Expand Down
Loading