Skip to content

Commit e480cd8

Browse files
committed
Enhance webhook integration to support SBOM and vulnerability events, refactor asset version service, and update related interfaces
Signed-off-by: Rafi <refaei.shikho@hotmail.com>
1 parent 0626572 commit e480cd8

10 files changed

Lines changed: 436 additions & 60 deletions

File tree

cmd/devguard/api/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ func BuildRouter(db core.DB) *echo.Echo {
417417
componentProjectRepository := repositories.NewComponentProjectRepository(db)
418418
componentService := component.NewComponentService(&depsDevService, componentProjectRepository, componentRepository)
419419

420-
assetVersionService := assetversion.NewService(assetVersionRepository, componentRepository, dependencyVulnRepository, firstPartyVulnRepository, dependencyVulnService, firstPartyVulnService, assetRepository, vulnEventRepository, &componentService)
420+
assetVersionService := assetversion.NewService(assetVersionRepository, componentRepository, dependencyVulnRepository, firstPartyVulnRepository, dependencyVulnService, firstPartyVulnService, assetRepository, projectRepository, orgRepository, vulnEventRepository, &componentService, thirdPartyIntegration)
421421
statisticsService := statistics.NewService(statisticsRepository, componentRepository, assetRiskAggregationRepository, dependencyVulnRepository, assetVersionRepository, projectRepository, repositories.NewProjectRiskHistoryRepository(db))
422422
invitationRepository := repositories.NewInvitationRepository(db)
423423

internal/core/assetversion/asset_version_service.go

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/l3montree-dev/devguard/internal/core"
1919
"github.com/l3montree-dev/devguard/internal/core/normalize"
2020
"github.com/l3montree-dev/devguard/internal/core/risk"
21+
"github.com/l3montree-dev/devguard/internal/core/vuln"
2122
"github.com/l3montree-dev/devguard/internal/database"
2223
"github.com/openvex/go-vex/pkg/vex"
2324

@@ -36,12 +37,15 @@ type service struct {
3637
firstPartyVulnService core.FirstPartyVulnService
3738
assetVersionRepository core.AssetVersionRepository
3839
assetRepository core.AssetRepository
40+
projectRepository core.ProjectRepository
41+
orgRepository core.OrganizationRepository
3942
vulnEventRepository core.VulnEventRepository
4043
componentService core.ComponentService
4144
httpClient *http.Client
45+
thirdPartyIntegration core.ThirdPartyIntegration
4246
}
4347

44-
func NewService(assetVersionRepository core.AssetVersionRepository, componentRepository core.ComponentRepository, dependencyVulnRepository core.DependencyVulnRepository, firstPartyVulnRepository core.FirstPartyVulnRepository, dependencyVulnService core.DependencyVulnService, firstPartyVulnService core.FirstPartyVulnService, assetRepository core.AssetRepository, vulnEventRepository core.VulnEventRepository, componentService core.ComponentService) *service {
48+
func NewService(assetVersionRepository core.AssetVersionRepository, componentRepository core.ComponentRepository, dependencyVulnRepository core.DependencyVulnRepository, firstPartyVulnRepository core.FirstPartyVulnRepository, dependencyVulnService core.DependencyVulnService, firstPartyVulnService core.FirstPartyVulnService, assetRepository core.AssetRepository, projectRepository core.ProjectRepository, orgRepository core.OrganizationRepository, vulnEventRepository core.VulnEventRepository, componentService core.ComponentService, thirdPartyIntegration core.ThirdPartyIntegration) *service {
4549
return &service{
4650
assetVersionRepository: assetVersionRepository,
4751
componentRepository: componentRepository,
@@ -53,6 +57,9 @@ func NewService(assetVersionRepository core.AssetVersionRepository, componentRep
5357
componentService: componentService,
5458
assetRepository: assetRepository,
5559
httpClient: &http.Client{},
60+
thirdPartyIntegration: thirdPartyIntegration,
61+
projectRepository: projectRepository,
62+
orgRepository: orgRepository,
5663
}
5764
}
5865

@@ -196,6 +203,29 @@ func (s *service) handleFirstPartyVulnResult(userID string, scannerID string, as
196203
return vuln.State == models.VulnStateOpen
197204
})
198205

206+
go func() {
207+
pro, err := s.projectRepository.GetProjectByAssetID(asset.ID)
208+
if err != nil {
209+
slog.Error("could not get project by asset ID", "err", err)
210+
return
211+
}
212+
org, err := s.orgRepository.Read(pro.OrganizationID)
213+
if err != nil {
214+
slog.Error("could not get organization by ID", "err", err)
215+
return
216+
}
217+
218+
if err = s.thirdPartyIntegration.HandleEvent(core.FirstPartyVulnsDetectedEvent{
219+
AssetVersion: core.ToAssetVersionObject(*assetVersion),
220+
Asset: core.ToAssetObject(asset),
221+
Project: core.ToProjectObject(pro),
222+
Org: core.ToOrgObject(org),
223+
Vulns: utils.Map(newVulns, vuln.FirstPartyVulnToDto),
224+
}); err != nil {
225+
slog.Error("could not handle first party vulnerabilities detected event", "err", err)
226+
}
227+
}()
228+
199229
return len(newVulns), len(fixedVulns), append(newVulns, comparison.InBoth...), nil
200230
}
201231

@@ -254,6 +284,32 @@ func (s *service) HandleScanResult(asset models.Asset, assetVersion *models.Asse
254284

255285
assetVersion.Metadata[scannerID] = models.ScannerInformation{LastScan: utils.Ptr(time.Now())}
256286

287+
go func() {
288+
pro, err := s.projectRepository.GetProjectByAssetID(asset.ID)
289+
if err != nil {
290+
slog.Error("could not get project by asset ID", "err", err)
291+
return
292+
}
293+
294+
org, err := s.orgRepository.Read(pro.OrganizationID)
295+
if err != nil {
296+
slog.Error("could not get organization by ID", "err", err)
297+
return
298+
}
299+
300+
if err = s.thirdPartyIntegration.HandleEvent(core.DependencyVulnsDetectedEvent{
301+
AssetVersion: core.ToAssetVersionObject(*assetVersion),
302+
Asset: core.ToAssetObject(asset),
303+
Project: core.ToProjectObject(pro),
304+
Org: core.ToOrgObject(org),
305+
306+
Vulns: utils.Map(opened, vuln.DependencyVulnToDto),
307+
}); err != nil {
308+
slog.Error("could not handle dependency vulnerabilities detected event", "err", err)
309+
}
310+
311+
}()
312+
257313
return opened, closed, newState, nil
258314
}
259315

@@ -419,14 +475,14 @@ func buildBomRefMap(bom normalize.SBOM) map[string]cdx.Component {
419475
return res
420476
}
421477

422-
func (s *service) UpdateSBOM(assetVersion models.AssetVersion, scannerID string, sbom normalize.SBOM) (bool, error) {
478+
func (s *service) UpdateSBOM(assetVersion models.AssetVersion, scannerID string, sbom normalize.SBOM) error {
423479

424480
sbomUpdated := false
425481

426482
// load the asset components
427483
assetComponents, err := s.componentRepository.LoadComponents(nil, assetVersion.Name, assetVersion.AssetID, "")
428484
if err != nil {
429-
return sbomUpdated, errors.Wrap(err, "could not load asset components")
485+
return errors.Wrap(err, "could not load asset components")
430486
}
431487

432488
existingComponentPurls := make(map[string]bool)
@@ -516,12 +572,12 @@ func (s *service) UpdateSBOM(assetVersion models.AssetVersion, scannerID string,
516572

517573
// make sure, that the components exist
518574
if err := s.componentRepository.CreateBatch(nil, componentsSlice); err != nil {
519-
return sbomUpdated, err
575+
return err
520576
}
521577

522578
sbomUpdated, err = s.componentRepository.HandleStateDiff(nil, assetVersion.Name, assetVersion.AssetID, assetComponents, dependencies, scannerID)
523579
if err != nil {
524-
return sbomUpdated, err
580+
return err
525581
}
526582

527583
// update the license information in the background
@@ -536,7 +592,43 @@ func (s *service) UpdateSBOM(assetVersion models.AssetVersion, scannerID string,
536592
}
537593
}()
538594

539-
return sbomUpdated, nil
595+
go func(sbomUpdated bool) {
596+
597+
if sbomUpdated {
598+
asset, err := s.assetRepository.Read(assetVersion.AssetID)
599+
if err != nil {
600+
slog.Error("could not read asset", "assetID", assetVersion.AssetID, "err", err)
601+
return
602+
}
603+
604+
pro, err := s.projectRepository.GetProjectByAssetID(asset.ID)
605+
if err != nil {
606+
slog.Error("could not get project by asset ID", "err", err)
607+
return
608+
}
609+
610+
org, err := s.orgRepository.Read(pro.OrganizationID)
611+
if err != nil {
612+
slog.Error("could not get organization by ID", "err", err)
613+
return
614+
}
615+
616+
if err = s.thirdPartyIntegration.HandleEvent(core.SBOMCreatedEvent{
617+
AssetVersion: core.ToAssetVersionObject(assetVersion),
618+
Asset: core.ToAssetObject(asset),
619+
Project: core.ToProjectObject(pro),
620+
Org: core.ToOrgObject(org),
621+
SBOM: sbom.GetCdxBom(),
622+
}); err != nil {
623+
slog.Error("could not handle SBOM updated event", "err", err)
624+
} else {
625+
slog.Info("handled SBOM updated event", "assetVersion", assetVersion.Name, "assetID", assetVersion.AssetID)
626+
}
627+
}
628+
629+
}(sbomUpdated)
630+
631+
return nil
540632
}
541633

542634
func (s *service) BuildSBOM(assetVersion models.AssetVersion, version string, organizationName string, components []models.ComponentDependency) *cdx.BOM {

internal/core/common_interfaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ type AssetVersionService interface {
252252
BuildVeX(asset models.Asset, assetVersion models.AssetVersion, orgName string, dependencyVulns []models.DependencyVuln) *cdx.BOM
253253
GetAssetVersionsByAssetID(assetID uuid.UUID) ([]models.AssetVersion, error)
254254
HandleFirstPartyVulnResult(asset models.Asset, assetVersion *models.AssetVersion, sarifScan common.SarifResult, scannerID string, userID string) (int, int, []models.FirstPartyVuln, error)
255-
UpdateSBOM(assetVersion models.AssetVersion, scannerID string, sbom normalize.SBOM) (bool, error)
255+
UpdateSBOM(assetVersion models.AssetVersion, scannerID string, sbom normalize.SBOM) error
256256
HandleScanResult(asset models.Asset, assetVersion *models.AssetVersion, vulns []models.VulnInPackage, scannerID string, userID string) (opened []models.DependencyVuln, closed []models.DependencyVuln, newState []models.DependencyVuln, err error)
257257
BuildOpenVeX(asset models.Asset, assetVersion models.AssetVersion, organizationSlug string, dependencyVulns []models.DependencyVuln) vex.VEX
258258
}

internal/core/daemon/scan_daemon.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"github.com/l3montree-dev/devguard/internal/core/integrations"
1212
"github.com/l3montree-dev/devguard/internal/core/integrations/githubint"
1313
"github.com/l3montree-dev/devguard/internal/core/integrations/gitlabint"
14+
"github.com/l3montree-dev/devguard/internal/core/integrations/jiraint"
15+
"github.com/l3montree-dev/devguard/internal/core/integrations/webhook"
1416
"github.com/l3montree-dev/devguard/internal/core/normalize"
1517
"github.com/l3montree-dev/devguard/internal/core/statistics"
1618
"github.com/l3montree-dev/devguard/internal/core/vuln"
@@ -47,17 +49,20 @@ func ScanAssetVersions(db core.DB, rbacProvider core.RBACProvider) error {
4749
gitlabOauth2Integrations,
4850
)
4951

52+
webhookIntegration := webhook.NewWebhookIntegration(db)
53+
54+
jiraIntegration := jiraint.NewJiraIntegration(db)
5055
gitlabIntegration := gitlabint.NewGitlabIntegration(db, gitlabOauth2Integrations, rbacProvider, gitlabClientFactory)
5156

5257
githubIntegration := githubint.NewGithubIntegration(db)
53-
thirdPartyIntegration := integrations.NewThirdPartyIntegrations(githubIntegration, gitlabIntegration)
58+
thirdPartyIntegration := integrations.NewThirdPartyIntegrations(githubIntegration, gitlabIntegration, jiraIntegration, webhookIntegration)
5459

5560
dependencyVulnService := vuln.NewService(dependencyVulnRepository, vulnEventRepository, assetRepository, cveRepository, orgRepository, projectRepository, thirdPartyIntegration, assetVersionRepository)
5661
firstPartyVulnService := vuln.NewFirstPartyVulnService(firstPartyVulnerabilityRepository, vulnEventRepository, assetRepository)
5762
depsDevService := vulndb.NewDepsDevService()
5863
componentService := component.NewComponentService(&depsDevService, componentProjectRepository, componentRepository)
5964

60-
assetVersionService := assetversion.NewService(assetVersionRepository, componentRepository, dependencyVulnRepository, firstPartyVulnerabilityRepository, dependencyVulnService, firstPartyVulnService, assetRepository, vulnEventRepository, &componentService)
65+
assetVersionService := assetversion.NewService(assetVersionRepository, componentRepository, dependencyVulnRepository, firstPartyVulnerabilityRepository, dependencyVulnService, firstPartyVulnService, assetRepository, projectRepository, orgRepository, vulnEventRepository, &componentService, thirdPartyIntegration)
6166

6267
statisticsService := statistics.NewService(statisticsRepository, componentRepository, assetRiskHistoryRepository, dependencyVulnRepository, assetVersionRepository, projectRepository, projectRiskHistoryRepository)
6368

internal/core/integrations/webhook/webhook_client.go

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,32 @@ package webhook
66
import (
77
"bytes"
88
"context"
9+
"encoding/json"
910
"fmt"
1011
"io"
1112
"net/http"
1213
"time"
1314

1415
cdx "github.com/CycloneDX/cyclonedx-go"
16+
"github.com/l3montree-dev/devguard/internal/core"
17+
"github.com/l3montree-dev/devguard/internal/core/vuln"
18+
)
19+
20+
type WebhookStruct struct {
21+
Organization core.OrgObject `json:"organization"`
22+
Project core.ProjectObject `json:"project"`
23+
Asset core.AssetObject `json:"asset"`
24+
AssetVersion core.AssetVersionObject `json:"assetVersion"`
25+
Payload any `json:"payload"`
26+
Type WebhookType `json:"type"`
27+
}
28+
29+
type WebhookType string
30+
31+
const (
32+
WebhookTypeSBOM WebhookType = "sbom"
33+
WebhookTypeFirstPartyVulnerabilities WebhookType = "firstPartyVulnerabilities"
34+
WebhookTypeDependencyVulnerabilities WebhookType = "dependencyVulnerabilities"
1535
)
1636

1737
type webhookClient struct {
@@ -33,6 +53,7 @@ func (c *webhookClient) CreateRequest(method, url string, body io.Reader) (*http
3353

3454
req, err := http.NewRequestWithContext(ctx, method, url, body)
3555
if err != nil {
56+
fmt.Println("Error creating request:", err)
3657
return nil, err
3758
}
3859

@@ -45,9 +66,77 @@ func (c *webhookClient) CreateRequest(method, url string, body io.Reader) (*http
4566
return http.DefaultClient.Do(req)
4667
}
4768

48-
func (c *webhookClient) SendSBOM(SBOM cdx.BOM) error {
69+
func (c *webhookClient) SendSBOM(SBOM cdx.BOM, org core.OrgObject, project core.ProjectObject, asset core.AssetObject, assetVersion core.AssetVersionObject) error {
70+
71+
body := WebhookStruct{
72+
Organization: org,
73+
Project: project,
74+
Asset: asset,
75+
AssetVersion: assetVersion,
76+
Payload: SBOM,
77+
Type: WebhookTypeSBOM,
78+
}
79+
80+
var buf bytes.Buffer
81+
err := json.NewEncoder(&buf).Encode(body)
82+
if err != nil {
83+
return err
84+
}
85+
86+
resp, err := c.CreateRequest("POST", c.URL, &buf)
87+
if err != nil {
88+
return err
89+
}
90+
defer resp.Body.Close()
91+
if resp.StatusCode != http.StatusOK {
92+
return fmt.Errorf("failed to send SBOM, status: %s", resp.Status)
93+
}
94+
95+
return nil
96+
}
97+
98+
func (c *webhookClient) SendFirstPartyVulnerabilities(vuln []vuln.FirstPartyVulnDTO, org core.OrgObject, project core.ProjectObject, asset core.AssetObject, assetVersion core.AssetVersionObject) error {
99+
100+
body := WebhookStruct{
101+
Organization: org,
102+
Project: project,
103+
Asset: asset,
104+
AssetVersion: assetVersion,
105+
Payload: vuln,
106+
Type: WebhookTypeFirstPartyVulnerabilities,
107+
}
108+
109+
var buf bytes.Buffer
110+
err := json.NewEncoder(&buf).Encode(body)
111+
if err != nil {
112+
return err
113+
}
114+
115+
resp, err := c.CreateRequest("POST", c.URL, &buf)
116+
if err != nil {
117+
return err
118+
}
119+
defer resp.Body.Close()
120+
if resp.StatusCode != http.StatusOK {
121+
return fmt.Errorf("failed to send vulnerability, status: %s,", resp.Status)
122+
}
123+
124+
return nil
125+
}
126+
127+
func (c *webhookClient) SendDependencyVulnerabilities(vuln []vuln.DependencyVulnDTO, org core.OrgObject, project core.ProjectObject, asset core.AssetObject, assetVersion core.AssetVersionObject) error {
128+
129+
body := WebhookStruct{
130+
Organization: org,
131+
Project: project,
132+
Asset: asset,
133+
AssetVersion: assetVersion,
134+
Payload: vuln,
135+
Type: WebhookTypeDependencyVulnerabilities,
136+
}
137+
49138
var buf bytes.Buffer
50-
err := cdx.NewBOMEncoder(&buf, cdx.BOMFileFormatJSON).Encode(&SBOM)
139+
err := json.NewEncoder(&buf).Encode(body)
51140
if err != nil {
52141
return err
53142
}
@@ -58,8 +147,7 @@ func (c *webhookClient) SendSBOM(SBOM cdx.BOM) error {
58147
}
59148
defer resp.Body.Close()
60149
if resp.StatusCode != http.StatusOK {
61-
body, _ := io.ReadAll(resp.Body)
62-
return fmt.Errorf("failed to send SBOM, status: %s, body: %s", resp.Status, body)
150+
return fmt.Errorf("failed to send vulnerability, status: %s", resp.Status)
63151
}
64152

65153
return nil

0 commit comments

Comments
 (0)