Skip to content

Commit 9666f6a

Browse files
committed
Add webhook integration support
Signed-off-by: Rafi <refaei.shikho@hotmail.com>
1 parent 79e8e2f commit 9666f6a

12 files changed

Lines changed: 384 additions & 7 deletions

File tree

cmd/devguard/api/api.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/l3montree-dev/devguard/internal/core/integrations/githubint"
3737
"github.com/l3montree-dev/devguard/internal/core/integrations/gitlabint"
3838
"github.com/l3montree-dev/devguard/internal/core/integrations/jiraint"
39+
"github.com/l3montree-dev/devguard/internal/core/integrations/webhook"
3940
"github.com/l3montree-dev/devguard/internal/core/intoto"
4041
"github.com/l3montree-dev/devguard/internal/core/org"
4142
"github.com/l3montree-dev/devguard/internal/core/pat"
@@ -367,6 +368,8 @@ func BuildRouter(db core.DB) *echo.Echo {
367368
panic(err)
368369
}
369370

371+
webhookIntegration := webhook.NewWebhookIntegration(db)
372+
370373
jiraIntegration := jiraint.NewJiraIntegration(db)
371374

372375
githubIntegration := githubint.NewGithubIntegration(db)
@@ -378,7 +381,7 @@ func BuildRouter(db core.DB) *echo.Echo {
378381
)
379382

380383
gitlabIntegration := gitlabint.NewGitlabIntegration(db, gitlabOauth2Integrations, casbinRBACProvider, gitlabClientFactory)
381-
thirdPartyIntegration := integrations.NewThirdPartyIntegrations(gitlabIntegration, githubIntegration, jiraIntegration)
384+
thirdPartyIntegration := integrations.NewThirdPartyIntegrations(gitlabIntegration, githubIntegration, jiraIntegration, webhookIntegration)
382385

383386
// init all repositories using the provided database
384387
patRepository := repositories.NewPATRepository(db)
@@ -541,6 +544,12 @@ func BuildRouter(db core.DB) *echo.Echo {
541544
organizationRouter.POST("/integrations/jira/test-and-save/", integrationController.TestAndSaveJiraIntegration, neededScope([]string{"manage"}))
542545
organizationRouter.DELETE("/integrations/jira/:jira_integration_id/", integrationController.DeleteJiraAccessToken, neededScope([]string{"manage"}))
543546

547+
organizationRouter.POST("/integrations/webhook/test-and-save/", integrationController.TestAndSaveWebhookIntegration, neededScope([]string{"manage"}))
548+
549+
organizationRouter.PUT("/integrations/webhook/test-and-save/", integrationController.UpdateWebhookIntegration, neededScope([]string{"manage"}))
550+
551+
organizationRouter.DELETE("/integrations/webhook/:id/", integrationController.DeleteWebhookIntegration, neededScope([]string{"manage"}))
552+
544553
organizationRouter.POST("/integrations/gitlab/test-and-save/", integrationController.TestAndSaveGitlabIntegration, neededScope([]string{"manage"}))
545554
organizationRouter.DELETE("/integrations/gitlab/:gitlab_integration_id/", integrationController.DeleteGitLabAccessToken, neededScope([]string{"manage"}))
546555
organizationRouter.GET("/integrations/repositories/", integrationController.ListRepositories)

internal/common/integrations_obj.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,13 @@ type JiraIntegrationDTO struct {
1414
ObfuscatedToken string `json:"obfuscatedToken"`
1515
UserEmail string `json:"userEmail"`
1616
}
17+
18+
type WebhookIntegrationDTO struct {
19+
ID string `json:"id"`
20+
Name string `json:"name"`
21+
Description string `json:"description"`
22+
URL string `json:"url"`
23+
Secret string `json:"secret"`
24+
SbomEnabled bool `json:"sbomEnabled"`
25+
VulnEnabled bool `json:"vulnEnabled"`
26+
}

internal/core/common_interfaces.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,14 @@ type JiraIntegrationRepository interface {
325325
GetClientByIntegrationID(integrationID uuid.UUID) (models.JiraIntegration, error)
326326
}
327327

328+
type WebhookIntegrationRepository interface {
329+
Save(tx DB, model *models.WebhookIntegration) error
330+
Read(id uuid.UUID) (models.WebhookIntegration, error)
331+
FindByOrganizationID(orgID uuid.UUID) ([]models.WebhookIntegration, error)
332+
Delete(tx DB, id uuid.UUID) error
333+
GetClientByIntegrationID(integrationID uuid.UUID) (models.WebhookIntegration, error)
334+
}
335+
328336
type GitlabIntegrationRepository interface {
329337
Save(tx DB, model *models.GitLabIntegration) error
330338
Read(id uuid.UUID) (models.GitLabIntegration, error)

internal/core/context_utils.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ func HasOrganization(c Context) bool {
139139
_, ok := c.Get("organization").(models.Org)
140140
return ok
141141
}
142-
142+
func HasProject(c Context) bool {
143+
_, ok := c.Get("project").(models.Project)
144+
return ok
145+
}
143146
func GetRBAC(ctx Context) AccessControl {
144147
return ctx.Get("rbac").(AccessControl)
145148
}

internal/core/integrations/integration_controller.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/l3montree-dev/devguard/internal/core/integrations/githubint"
2323
"github.com/l3montree-dev/devguard/internal/core/integrations/gitlabint"
2424
"github.com/l3montree-dev/devguard/internal/core/integrations/jiraint"
25+
"github.com/l3montree-dev/devguard/internal/core/integrations/webhook"
2526
)
2627

2728
type integrationController struct {
@@ -94,6 +95,50 @@ func (c *integrationController) TestAndSaveGitlabIntegration(ctx core.Context) e
9495
return nil
9596
}
9697

98+
func (c *integrationController) DeleteWebhookIntegration(ctx core.Context) error {
99+
thirdPartyIntegration := core.GetThirdPartyIntegration(ctx)
100+
wh := thirdPartyIntegration.GetIntegration(core.WebhookIntegrationID)
101+
if wh == nil {
102+
return ctx.JSON(404, "Webhook integration not enabled")
103+
}
104+
105+
if err := wh.(*webhook.WebhookIntegration).Delete(ctx); err != nil {
106+
slog.Error("could not delete webhook integration", "err", err)
107+
return err
108+
}
109+
110+
return nil
111+
}
112+
113+
func (c *integrationController) UpdateWebhookIntegration(ctx core.Context) error {
114+
thirdPartyIntegration := core.GetThirdPartyIntegration(ctx)
115+
wh := thirdPartyIntegration.GetIntegration(core.WebhookIntegrationID)
116+
if wh == nil {
117+
return ctx.JSON(404, "Webhook integration not enabled")
118+
}
119+
120+
if err := wh.(*webhook.WebhookIntegration).Update(ctx); err != nil {
121+
slog.Error("could not update webhook integration", "err", err)
122+
return err
123+
}
124+
125+
return nil
126+
}
127+
128+
func (c *integrationController) TestAndSaveWebhookIntegration(ctx core.Context) error {
129+
thirdPartyIntegration := core.GetThirdPartyIntegration(ctx)
130+
wh := thirdPartyIntegration.GetIntegration(core.WebhookIntegrationID)
131+
if wh == nil {
132+
return ctx.JSON(404, "Webhook integration not enabled")
133+
}
134+
135+
if err := wh.(*webhook.WebhookIntegration).TestAndSave(ctx); err != nil {
136+
slog.Error("could not test GitLab integration", "err", err)
137+
return err
138+
}
139+
140+
return nil
141+
}
97142
func (c *integrationController) TestAndSaveJiraIntegration(ctx core.Context) error {
98143
thirdPartyIntegration := core.GetThirdPartyIntegration(ctx)
99144
gl := thirdPartyIntegration.GetIntegration(core.JiraIntegrationID)
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Copyright 2025 l3montree GmbH.
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
package webhook
5+
6+
import (
7+
"context"
8+
"log/slog"
9+
10+
"github.com/google/uuid"
11+
"github.com/l3montree-dev/devguard/internal/common"
12+
"github.com/l3montree-dev/devguard/internal/core"
13+
"github.com/l3montree-dev/devguard/internal/database/models"
14+
"github.com/l3montree-dev/devguard/internal/database/repositories"
15+
)
16+
17+
type WebhookIntegration struct {
18+
webhookRepository core.WebhookIntegrationRepository
19+
}
20+
21+
var _ core.ThirdPartyIntegration = &WebhookIntegration{}
22+
23+
func NewWebhookIntegration(db core.DB) *WebhookIntegration {
24+
webhookRepository := repositories.NewWebhookRepository(db)
25+
return &WebhookIntegration{
26+
webhookRepository: webhookRepository,
27+
}
28+
}
29+
30+
func (w *WebhookIntegration) Delete(ctx core.Context) error {
31+
id := ctx.Param("id")
32+
if id == "" {
33+
return ctx.JSON(400, "id is required")
34+
}
35+
36+
uuidID, err := uuid.Parse(id)
37+
if err != nil {
38+
return ctx.JSON(400, "invalid id format")
39+
}
40+
41+
if err := w.webhookRepository.Delete(nil, uuidID); err != nil {
42+
slog.Error("failed to delete webhook integration", "err", err)
43+
return ctx.JSON(500, "failed to delete webhook integration")
44+
}
45+
return ctx.JSON(200, "Webhook integration deleted successfully")
46+
}
47+
48+
func (w *WebhookIntegration) Update(ctx core.Context) error {
49+
var data struct {
50+
ID string `json:"id"`
51+
Name string `json:"name"`
52+
Description string `json:"description"`
53+
URL string `json:"url"`
54+
Secret string `json:"secret"`
55+
SbomEnabled bool `json:"sbomEnabled"`
56+
VulnEnabled bool `json:"vulnEnabled"`
57+
}
58+
59+
if err := ctx.Bind(&data); err != nil {
60+
return ctx.JSON(400, "invalid request data")
61+
}
62+
if data.URL == "" {
63+
return ctx.JSON(400, "url is required")
64+
}
65+
66+
uuidID, err := uuid.Parse(data.ID)
67+
if err != nil {
68+
return ctx.JSON(400, "invalid id format")
69+
}
70+
webhookIntegration := &models.WebhookIntegration{
71+
72+
Model: models.Model{
73+
ID: uuidID,
74+
},
75+
Name: &data.Name,
76+
Description: &data.Description,
77+
URL: data.URL,
78+
Secret: &data.Secret,
79+
SbomEnabled: data.SbomEnabled,
80+
VulnEnabled: data.VulnEnabled,
81+
OrgID: core.GetOrg(ctx).GetID(),
82+
}
83+
84+
if err := w.webhookRepository.Save(nil, webhookIntegration); err != nil {
85+
slog.Error("failed to update webhook integration", "err", err)
86+
return ctx.JSON(500, "failed to update webhook integration")
87+
}
88+
return ctx.JSON(200, common.WebhookIntegrationDTO{
89+
ID: webhookIntegration.ID.String(),
90+
Name: *webhookIntegration.Name,
91+
Description: *webhookIntegration.Description,
92+
URL: webhookIntegration.URL,
93+
Secret: *webhookIntegration.Secret,
94+
SbomEnabled: webhookIntegration.SbomEnabled,
95+
VulnEnabled: webhookIntegration.VulnEnabled,
96+
})
97+
}
98+
func (w *WebhookIntegration) TestAndSave(ctx core.Context) error {
99+
var data struct {
100+
Name string `json:"name"`
101+
Description string `json:"description"`
102+
URL string `json:"url"`
103+
Secret string `json:"secret"`
104+
SbomEnabled bool `json:"sbomEnabled"`
105+
VulnEnabled bool `json:"vulnEnabled"`
106+
}
107+
108+
if err := ctx.Bind(&data); err != nil {
109+
return ctx.JSON(400, "invalid request data")
110+
}
111+
if data.URL == "" {
112+
return ctx.JSON(400, "url is required")
113+
}
114+
115+
/* projectID := uuid.Nil
116+
if ok := core.HasProject(ctx); ok {
117+
project := core.GetProject(ctx)
118+
projectID = project.ID
119+
} */
120+
121+
webhookIntegration := &models.WebhookIntegration{
122+
Name: &data.Name,
123+
Description: &data.Description,
124+
URL: data.URL,
125+
Secret: &data.Secret,
126+
SbomEnabled: data.SbomEnabled,
127+
VulnEnabled: data.VulnEnabled,
128+
OrgID: core.GetOrg(ctx).GetID(),
129+
/* ProjectID: &projectID, */
130+
}
131+
132+
if err := w.webhookRepository.Save(nil, webhookIntegration); err != nil {
133+
slog.Error("failed to save webhook integration", "err", err)
134+
return ctx.JSON(500, "failed to save webhook integration")
135+
}
136+
return ctx.JSON(200, common.WebhookIntegrationDTO{
137+
ID: webhookIntegration.ID.String(),
138+
Name: *webhookIntegration.Name,
139+
Description: *webhookIntegration.Description,
140+
URL: webhookIntegration.URL,
141+
Secret: *webhookIntegration.Secret,
142+
SbomEnabled: webhookIntegration.SbomEnabled,
143+
VulnEnabled: webhookIntegration.VulnEnabled,
144+
})
145+
}
146+
147+
func (w *WebhookIntegration) HandleEvent(event any) error {
148+
return nil
149+
}
150+
151+
func (w *WebhookIntegration) WantsToHandleWebhook(ctx core.Context) bool {
152+
// Logic to determine if this integration wants to handle the webhook
153+
return true
154+
}
155+
156+
func (w *WebhookIntegration) HandleWebhook(ctx core.Context) error {
157+
// Logic to handle the webhook
158+
return nil
159+
}
160+
161+
func (w *WebhookIntegration) ListOrgs(ctx core.Context) ([]models.Org, error) {
162+
// Logic to list organizations
163+
return nil, nil
164+
}
165+
166+
func (w *WebhookIntegration) ListGroups(ctx core.Context, userID string, providerID string) ([]models.Project, error) {
167+
// Logic to list groups
168+
return nil, nil
169+
}
170+
171+
func (w *WebhookIntegration) ListProjects(ctx core.Context, userID string, providerID string, groupID string) ([]models.Asset, error) {
172+
// Logic to list projects
173+
return nil, nil
174+
}
175+
176+
func (w *WebhookIntegration) ListRepositories(ctx core.Context) ([]core.Repository, error) {
177+
// Logic to list repositories
178+
return nil, nil
179+
}
180+
181+
func (w *WebhookIntegration) HasAccessToExternalEntityProvider(ctx core.Context, externalEntityProviderID string) (bool, error) {
182+
// Logic to check access to external entity provider
183+
return false, nil
184+
}
185+
186+
func (w *WebhookIntegration) GetRoleInGroup(ctx context.Context, userID string, providerID string, groupID string) (string, error) {
187+
// Logic to get role in group
188+
return "", nil
189+
}
190+
191+
func (w *WebhookIntegration) GetRoleInProject(ctx context.Context, userID string, providerID string, projectID string) (string, error) {
192+
// Logic to get role in project
193+
return "", nil
194+
}
195+
196+
func (w *WebhookIntegration) CreateIssue(ctx context.Context, asset models.Asset, assetVersionName string, vuln models.Vuln, projectSlug string, orgSlug string, justification string, userID string) error {
197+
// Logic to create an issue
198+
return nil
199+
}
200+
201+
func (w *WebhookIntegration) UpdateIssue(ctx context.Context, asset models.Asset, vuln models.Vuln) error {
202+
// Logic to update an issue
203+
return nil
204+
}
205+
206+
func (w *WebhookIntegration) GetUsers(org models.Org) []core.User {
207+
// Logic to get users in an organization
208+
return nil
209+
}
210+
211+
func (w *WebhookIntegration) GetID() core.IntegrationID {
212+
// Return the integration ID for this webhook integration
213+
return core.WebhookIntegrationID
214+
}

internal/core/org/org_dto.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ type OrgDTO struct {
177177

178178
JiraIntegrations []common.JiraIntegrationDTO `json:"jiraIntegrations" gorm:"foreignKey:OrgID;"`
179179

180+
Webhooks []common.WebhookIntegrationDTO `json:"webhooks" gorm:"foreignKey:OrgID;"`
181+
180182
IsPublic bool `json:"isPublic" gorm:"default:false;"`
181183

182184
ConfigFiles map[string]any `json:"configFiles"`
@@ -204,6 +206,18 @@ func obfuscateJiraIntegrations(integration models.JiraIntegration) common.JiraIn
204206
}
205207
}
206208

209+
func obfuscateWebhookIntegrations(integration models.WebhookIntegration) common.WebhookIntegrationDTO {
210+
return common.WebhookIntegrationDTO{
211+
ID: integration.ID.String(),
212+
Name: *integration.Name,
213+
Description: *integration.Description,
214+
URL: integration.URL,
215+
Secret: *integration.Secret,
216+
SbomEnabled: integration.SbomEnabled,
217+
VulnEnabled: integration.VulnEnabled,
218+
}
219+
}
220+
207221
func fromModel(org models.Org) OrgDTO {
208222
return OrgDTO{
209223
Model: org.Model,
@@ -224,6 +238,7 @@ func fromModel(org models.Org) OrgDTO {
224238
GithubAppInstallations: org.GithubAppInstallations,
225239
GitLabIntegrations: utils.Map(org.GitLabIntegrations, obfuscateGitLabIntegrations),
226240
JiraIntegrations: utils.Map(org.JiraIntegrations, obfuscateJiraIntegrations),
241+
Webhooks: utils.Map(org.Webhooks, obfuscateWebhookIntegrations),
227242
ConfigFiles: org.ConfigFiles,
228243
Language: org.Language,
229244
ExternalEntityProviderID: org.ExternalEntityProviderID,

0 commit comments

Comments
 (0)