Skip to content

Commit 6ff6066

Browse files
committed
adds functionality to test webhooks - disabled first party vulnerability webhooks for now
1 parent b220537 commit 6ff6066

4 files changed

Lines changed: 301 additions & 7 deletions

File tree

cmd/devguard/api/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,8 @@ func BuildRouter(db core.DB) *echo.Echo {
588588

589589
organizationRouter.POST("/integrations/webhook/test-and-save/", webhookIntegration.Save, neededScope([]string{"manage"}), accessControlMiddleware(core.ObjectOrganization, core.ActionUpdate))
590590

591+
organizationRouter.POST("/integrations/webhook/test/", webhookIntegration.Test, neededScope([]string{"manage"}), accessControlMiddleware(core.ObjectOrganization, core.ActionRead))
592+
591593
organizationRouter.PUT("/integrations/webhook/:id/", webhookIntegration.Update, neededScope([]string{"manage"}), accessControlMiddleware(core.ObjectOrganization, core.ActionUpdate))
592594

593595
organizationRouter.DELETE("/integrations/webhook/:id/", webhookIntegration.Delete, neededScope([]string{"manage"}), accessControlMiddleware(core.ObjectOrganization, core.ActionUpdate))
@@ -611,6 +613,9 @@ func BuildRouter(db core.DB) *echo.Echo {
611613
projectRouter.GET("/", projectController.Read)
612614

613615
projectRouter.POST("/integrations/webhook/test-and-save/", webhookIntegration.Save, neededScope([]string{"manage"}), projectScopedRBAC(core.ObjectProject, core.ActionUpdate))
616+
617+
projectRouter.POST("/integrations/webhook/test/", webhookIntegration.Test, neededScope([]string{"manage"}), projectScopedRBAC(core.ObjectProject, core.ActionRead))
618+
614619
projectRouter.PUT("/integrations/webhook/:id/", webhookIntegration.Update, neededScope([]string{"manage"}), projectScopedRBAC(core.ObjectProject, core.ActionUpdate))
615620
projectRouter.DELETE("/integrations/webhook/:id/", webhookIntegration.Delete, neededScope([]string{"manage"}), projectScopedRBAC(core.ObjectProject, core.ActionUpdate))
616621

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ services:
1010
# dockerfile: Dockerfile.postgres
1111
env_file: .env
1212
ports:
13-
- "5432:5432"
13+
- "5433:5432"
1414
volumes:
1515
- postgres:/var/lib/postgresql/data
1616
- ./initdb.sql:/docker-entrypoint-initdb.d/init.sql

internal/core/integrations/webhook/webhook_client.go

Lines changed: 194 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
cdx "github.com/CycloneDX/cyclonedx-go"
1616
"github.com/l3montree-dev/devguard/internal/core"
1717
"github.com/l3montree-dev/devguard/internal/core/vuln"
18+
"github.com/l3montree-dev/devguard/internal/database/models"
1819
)
1920

2021
type WebhookStruct struct {
@@ -32,6 +33,16 @@ const (
3233
WebhookTypeSBOM WebhookType = "sbom"
3334
WebhookTypeFirstPartyVulnerabilities WebhookType = "firstPartyVulnerabilities"
3435
WebhookTypeDependencyVulnerabilities WebhookType = "dependencyVulnerabilities"
36+
WebhookTypeTest WebhookType = "test"
37+
)
38+
39+
type TestPayloadType string
40+
41+
const (
42+
TestPayloadTypeEmpty TestPayloadType = "empty"
43+
TestPayloadTypeSampleSBOM TestPayloadType = "sampleSbom"
44+
TestPayloadTypeSampleDependencyVulns TestPayloadType = "sampleDependencyVulns"
45+
TestPayloadTypeSampleFirstPartyVulns TestPayloadType = "sampleFirstPartyVulns"
3546
)
3647

3748
type webhookClient struct {
@@ -96,8 +107,9 @@ func (c *webhookClient) SendSBOM(SBOM cdx.BOM, org core.OrgObject, project core.
96107
}
97108

98109
func (c *webhookClient) SendFirstPartyVulnerabilities(vuln []vuln.FirstPartyVulnDTO, org core.OrgObject, project core.ProjectObject, asset core.AssetObject, assetVersion core.AssetVersionObject) error {
110+
return nil
99111

100-
body := WebhookStruct{
112+
/*body := WebhookStruct{
101113
Organization: org,
102114
Project: project,
103115
Asset: asset,
@@ -121,7 +133,7 @@ func (c *webhookClient) SendFirstPartyVulnerabilities(vuln []vuln.FirstPartyVuln
121133
return fmt.Errorf("failed to send vulnerability, status: %s,", resp.Status)
122134
}
123135
124-
return nil
136+
return nil*/
125137
}
126138

127139
func (c *webhookClient) SendDependencyVulnerabilities(vuln []vuln.DependencyVulnDTO, org core.OrgObject, project core.ProjectObject, asset core.AssetObject, assetVersion core.AssetVersionObject) error {
@@ -152,3 +164,183 @@ func (c *webhookClient) SendDependencyVulnerabilities(vuln []vuln.DependencyVuln
152164

153165
return nil
154166
}
167+
168+
func (c *webhookClient) SendTest(org core.OrgObject, project core.ProjectObject, asset core.AssetObject, assetVersion core.AssetVersionObject, payloadType TestPayloadType) error {
169+
170+
var payload any
171+
var webhookType WebhookType
172+
173+
switch payloadType {
174+
case TestPayloadTypeEmpty:
175+
payload = map[string]any{
176+
"message": "This is a test webhook from DevGuard",
177+
"timestamp": time.Now().UTC().Format(time.RFC3339),
178+
}
179+
webhookType = WebhookTypeTest
180+
181+
case TestPayloadTypeSampleSBOM:
182+
payload = createSampleSBOM()
183+
webhookType = WebhookTypeSBOM
184+
185+
case TestPayloadTypeSampleDependencyVulns:
186+
payload = createSampleDependencyVulns()
187+
webhookType = WebhookTypeDependencyVulnerabilities
188+
189+
case TestPayloadTypeSampleFirstPartyVulns:
190+
payload = createSampleFirstPartyVulns()
191+
webhookType = WebhookTypeFirstPartyVulnerabilities
192+
193+
default:
194+
payload = map[string]any{
195+
"message": "This is a test webhook from DevGuard",
196+
"timestamp": time.Now().UTC().Format(time.RFC3339),
197+
}
198+
webhookType = WebhookTypeTest
199+
}
200+
201+
body := WebhookStruct{
202+
Organization: org,
203+
Project: project,
204+
Asset: asset,
205+
AssetVersion: assetVersion,
206+
Payload: payload,
207+
Type: webhookType,
208+
}
209+
210+
var buf bytes.Buffer
211+
err := json.NewEncoder(&buf).Encode(body)
212+
if err != nil {
213+
return err
214+
}
215+
216+
resp, err := c.CreateRequest("POST", c.URL, &buf)
217+
if err != nil {
218+
return err
219+
}
220+
defer resp.Body.Close()
221+
if resp.StatusCode != http.StatusOK {
222+
return fmt.Errorf("failed to send test webhook, status: %s", resp.Status)
223+
}
224+
225+
return nil
226+
}
227+
228+
func createSampleSBOM() cdx.BOM {
229+
return cdx.BOM{
230+
BOMFormat: "CycloneDX",
231+
SpecVersion: cdx.SpecVersion1_4,
232+
SerialNumber: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
233+
Version: 1,
234+
Metadata: &cdx.Metadata{
235+
Timestamp: time.Now().UTC().Format(time.RFC3339),
236+
Component: &cdx.Component{
237+
Type: cdx.ComponentTypeApplication,
238+
Name: "example-web-app",
239+
Version: "1.2.3",
240+
PackageURL: "pkg:docker/example/web-app@1.2.3",
241+
},
242+
},
243+
Components: &[]cdx.Component{
244+
{
245+
Type: cdx.ComponentTypeLibrary,
246+
Name: "express",
247+
Version: "4.18.2",
248+
PackageURL: "pkg:npm/express@4.18.2",
249+
Licenses: &cdx.Licenses{
250+
{License: &cdx.License{ID: "MIT"}},
251+
},
252+
},
253+
{
254+
Type: cdx.ComponentTypeLibrary,
255+
Name: "log4j-core",
256+
Version: "2.14.1",
257+
PackageURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
258+
Licenses: &cdx.Licenses{
259+
{License: &cdx.License{ID: "Apache-2.0"}},
260+
},
261+
},
262+
},
263+
}
264+
}
265+
266+
func createSampleDependencyVulns() []vuln.DependencyVulnDTO {
267+
cve := "CVE-2021-44228"
268+
purl := "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"
269+
fixedVersion := "2.15.0"
270+
depth := 2
271+
risk := 95
272+
rawRisk := 9.8
273+
priority := 1
274+
effort := 4
275+
276+
cveData := &models.CVE{
277+
CVE: "CVE-2021-44228",
278+
Description: "Apache Log4j2 <=2.14.1 JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints.",
279+
CVSS: 10.0,
280+
Severity: models.SeverityCritical,
281+
AttackVector: "Network",
282+
AttackComplexity: "Low",
283+
PrivilegesRequired: "None",
284+
UserInteraction: "None",
285+
Scope: "Changed",
286+
ConfidentialityImpact: "High",
287+
IntegrityImpact: "High",
288+
AvailabilityImpact: "High",
289+
Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
290+
}
291+
292+
return []vuln.DependencyVulnDTO{
293+
{
294+
ID: "dep-vuln-001",
295+
ScannerIDs: "trivy",
296+
AssetVersionName: "v1.2.3",
297+
AssetID: "asset-12345",
298+
State: models.VulnStateOpen,
299+
CVEID: &cve,
300+
CVE: cveData,
301+
ComponentPurl: &purl,
302+
ComponentDepth: &depth,
303+
ComponentFixedVersion: &fixedVersion,
304+
Effort: &effort,
305+
RiskAssessment: &risk,
306+
RawRiskAssessment: &rawRisk,
307+
Priority: &priority,
308+
LastDetected: time.Now(),
309+
CreatedAt: time.Now().Add(-24 * time.Hour),
310+
RiskRecalculatedAt: time.Now(),
311+
},
312+
}
313+
}
314+
315+
func createSampleFirstPartyVulns() []vuln.FirstPartyVulnDTO {
316+
message := "SQL injection vulnerability detected"
317+
318+
return []vuln.FirstPartyVulnDTO{
319+
{
320+
ID: "fpv-001",
321+
ScannerIDs: "semgrep",
322+
Message: &message,
323+
AssetVersionName: "v1.2.3",
324+
AssetID: "asset-12345",
325+
State: models.VulnStateOpen,
326+
RuleID: "javascript.lang.security.audit.sqli",
327+
URI: "src/auth/login.js",
328+
SnippetContents: []models.SnippetContent{
329+
{
330+
StartLine: 42,
331+
EndLine: 45,
332+
Snippet: `const query = "SELECT * FROM users WHERE username = '" + username + "'";`,
333+
},
334+
},
335+
CreatedAt: time.Now(),
336+
Commit: "abc123",
337+
Author: "Developer",
338+
RuleName: "SQL Injection Detection",
339+
RuleDescription: "Detects SQL injection vulnerabilities",
340+
RuleProperties: map[string]any{
341+
"severity": "HIGH",
342+
"cwe": "CWE-89",
343+
},
344+
},
345+
}
346+
}

internal/core/integrations/webhook/webhook_integration.go

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package webhook
55

66
import (
77
"context"
8+
"fmt"
89
"log/slog"
910

1011
"github.com/google/uuid"
@@ -152,6 +153,106 @@ func (w *WebhookIntegration) Save(ctx core.Context) error {
152153
})
153154
}
154155

156+
func (w *WebhookIntegration) Test(ctx core.Context) error {
157+
var data struct {
158+
URL string `json:"url"`
159+
Secret string `json:"secret"`
160+
PayloadType string `json:"payloadType"`
161+
}
162+
163+
if err := ctx.Bind(&data); err != nil {
164+
return ctx.JSON(400, "invalid request data")
165+
}
166+
if data.URL == "" {
167+
return ctx.JSON(400, "url is required")
168+
}
169+
170+
// Default to empty payload if not specified
171+
if data.PayloadType == "" {
172+
data.PayloadType = "empty"
173+
}
174+
175+
// Validate payload type
176+
var payloadType TestPayloadType
177+
switch data.PayloadType {
178+
case "empty":
179+
payloadType = TestPayloadTypeEmpty
180+
case "sampleSbom":
181+
payloadType = TestPayloadTypeSampleSBOM
182+
case "sampleDependencyVulns":
183+
payloadType = TestPayloadTypeSampleDependencyVulns
184+
case "sampleFirstPartyVulns":
185+
payloadType = TestPayloadTypeSampleFirstPartyVulns
186+
default:
187+
return ctx.JSON(400, map[string]string{
188+
"error": "Invalid payload type. Supported types: empty, sampleSbom, sampleDependencyVulns, sampleFirstPartyVulns",
189+
})
190+
}
191+
192+
// Create example objects for testing
193+
org := core.ToOrgObject(core.GetOrg(ctx))
194+
195+
// For assets and projects, we'll use example data if not available in context
196+
var project core.ProjectObject
197+
var asset core.AssetObject
198+
var assetVersion core.AssetVersionObject
199+
200+
if core.HasProject(ctx) {
201+
project = core.ToProjectObject(core.GetProject(ctx))
202+
} else {
203+
// Create example project data
204+
project = core.ProjectObject{
205+
ID: uuid.New(),
206+
Name: "Example Project",
207+
Slug: "example-project",
208+
Description: "Example project for webhook testing",
209+
IsPublic: false,
210+
Type: "application",
211+
}
212+
}
213+
214+
// Create example asset and asset version data for testing
215+
asset = core.AssetObject{
216+
ID: uuid.New(),
217+
Name: "Example Asset",
218+
Slug: "example-asset",
219+
Description: "Example asset for webhook testing",
220+
ProjectID: project.ID,
221+
AvailabilityRequirement: "high",
222+
IntegrityRequirement: "high",
223+
ConfidentialityRequirement: "high",
224+
ReachableFromInternet: false,
225+
}
226+
227+
assetVersion = core.AssetVersionObject{
228+
Name: "example-version",
229+
AssetID: asset.ID,
230+
Slug: "example-version",
231+
DefaultBranch: true,
232+
Type: "branch",
233+
}
234+
235+
// Create webhook client and send test
236+
var secret *string
237+
if data.Secret != "" {
238+
secret = &data.Secret
239+
}
240+
241+
client := NewWebhookClient(data.URL, secret)
242+
243+
if err := client.SendTest(org, project, asset, assetVersion, payloadType); err != nil {
244+
slog.Error("failed to send test webhook", "err", err)
245+
return ctx.JSON(400, map[string]string{
246+
"error": fmt.Sprintf("Webhook test failed: %s", err.Error()),
247+
})
248+
}
249+
250+
return ctx.JSON(200, map[string]string{
251+
"message": "Test webhook sent successfully",
252+
"payloadType": data.PayloadType,
253+
})
254+
}
255+
155256
func (w *WebhookIntegration) HandleEvent(event any) error {
156257

157258
switch event := event.(type) {
@@ -162,10 +263,6 @@ func (w *WebhookIntegration) HandleEvent(event any) error {
162263
slog.Error("failed to find webhooks", "err", err)
163264
return err
164265
}
165-
if err != nil {
166-
slog.Error("failed to find webhooks for SBOM created event", "err", err)
167-
return err
168-
}
169266

170267
for _, webhook := range webhooks {
171268
client := NewWebhookClient(webhook.URL, webhook.Secret)

0 commit comments

Comments
 (0)