Skip to content

Commit b85d45c

Browse files
authored
chore: encapsulate dependency-track biz code (#51)
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent fd7d93e commit b85d45c

File tree

17 files changed

+345
-224
lines changed

17 files changed

+345
-224
lines changed

app/controlplane/cmd/wire.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package main
2222

2323
import (
2424
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
25+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/integration"
2526
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
2627
"github.com/chainloop-dev/chainloop/app/controlplane/internal/data"
2728
"github.com/chainloop-dev/chainloop/app/controlplane/internal/server"
@@ -40,6 +41,7 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger) (*app, func(
4041
server.ProviderSet,
4142
data.ProviderSet,
4243
biz.ProviderSet,
44+
integration.ProviderSet,
4345
service.ProviderSet,
4446
wire.Bind(new(backend.Provider), new(*oci.BackendProvider)),
4547
wire.Bind(new(biz.CASClient), new(*biz.CASClientUseCase)),

app/controlplane/cmd/wire_gen.go

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/biz/errors.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,18 @@ func IsErrInvalidUUID(err error) bool {
5252
return errors.As(err, &ErrInvalidUUID{})
5353
}
5454

55-
type errValidation struct {
55+
type ErrValidation struct {
5656
err error
5757
}
5858

59-
func newErrValidation(err error) errValidation {
60-
return errValidation{err}
59+
func NewErrValidation(err error) ErrValidation {
60+
return ErrValidation{err}
6161
}
6262

63-
func (e errValidation) Error() string {
63+
func (e ErrValidation) Error() string {
6464
return fmt.Sprintf("validation error: %s", e.err.Error())
6565
}
6666

6767
func IsErrValidation(err error) bool {
68-
return errors.As(err, &errValidation{})
68+
return errors.As(err, &ErrValidation{})
6969
}

app/controlplane/internal/biz/integration.go

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ type IntegrationUseCase struct {
7373
logger *log.Helper
7474
}
7575

76-
const DependencyTrackKind = "Dependency-Track"
77-
7876
type NewIntegrationUseCaseOpts struct {
7977
IRepo IntegrationRepo
8078
IaRepo IntegrationAttachmentRepo
@@ -91,34 +89,14 @@ func NewIntegrationUseCase(opts *NewIntegrationUseCaseOpts) *IntegrationUseCase
9189
return &IntegrationUseCase{opts.IRepo, opts.IaRepo, opts.WfRepo, opts.CredsRW, servicelogger.ScopedHelper(opts.Logger, "biz/integration")}
9290
}
9391

94-
func (uc *IntegrationUseCase) AddDependencyTrack(ctx context.Context, orgID, host, apiKey string, enableProjectCreation bool) (*Integration, error) {
92+
// Persist the integration with its configuration in the database
93+
func (uc *IntegrationUseCase) Create(ctx context.Context, orgID, kind string, secretID string, config *v1.IntegrationConfig) (*Integration, error) {
9594
orgUUID, err := uuid.Parse(orgID)
9695
if err != nil {
9796
return nil, NewErrInvalidUUID(err)
9897
}
9998

100-
// Validate Credentials before saving them
101-
creds := &credentials.APICreds{Host: host, Key: apiKey}
102-
if err := creds.Validate(); err != nil {
103-
return nil, newErrValidation(err)
104-
}
105-
106-
// Create the secret in the external secrets manager
107-
secretID, err := uc.credsRW.SaveCredentials(ctx, orgID, creds)
108-
if err != nil {
109-
return nil, fmt.Errorf("storing the credentials: %w", err)
110-
}
111-
112-
c := &v1.IntegrationConfig{
113-
Config: &v1.IntegrationConfig_DependencyTrack_{
114-
DependencyTrack: &v1.IntegrationConfig_DependencyTrack{
115-
AllowAutoCreate: enableProjectCreation, Domain: host,
116-
},
117-
},
118-
}
119-
120-
// Persist data
121-
return uc.integrationRepo.Create(ctx, orgUUID, DependencyTrackKind, secretID, c)
99+
return uc.integrationRepo.Create(ctx, orgUUID, kind, secretID, config)
122100
}
123101

124102
func (uc *IntegrationUseCase) List(ctx context.Context, orgID string) ([]*Integration, error) {
@@ -217,7 +195,7 @@ func (uc *IntegrationUseCase) AttachToWorkflow(ctx context.Context, opts *Attach
217195

218196
// Check that the provided attachConfiguration is compatible with the referred integration
219197
if err := validateAttachment(ctx, integration, uc.credsRW, integration.Config, opts.Config); err != nil {
220-
return nil, newErrValidation(err)
198+
return nil, NewErrValidation(err)
221199
}
222200

223201
return uc.integrationARepo.Create(ctx, integrationUUID, workflowUUID, opts.Config)
@@ -285,7 +263,7 @@ func validateAttachment(ctx context.Context, integration *Integration, credsR cr
285263
}
286264

287265
if err := creds.Validate(); err != nil {
288-
return newErrValidation(err)
266+
return NewErrValidation(err)
289267
}
290268

291269
// Instantiate an actual uploader to see if it would work with the current configuration
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//
2+
// Copyright 2023 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package dependencytrack
17+
18+
import (
19+
"bytes"
20+
"context"
21+
"fmt"
22+
"io"
23+
"sync"
24+
"time"
25+
26+
"github.com/cenkalti/backoff/v4"
27+
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
28+
contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
29+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
30+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/integrations/dependencytrack"
31+
"github.com/chainloop-dev/chainloop/internal/attestation/renderer"
32+
"github.com/chainloop-dev/chainloop/internal/blobmanager/oci"
33+
"github.com/chainloop-dev/chainloop/internal/credentials"
34+
"github.com/chainloop-dev/chainloop/internal/servicelogger"
35+
"github.com/go-kratos/kratos/v2/log"
36+
"github.com/go-openapi/errors"
37+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
38+
)
39+
40+
type Integration struct {
41+
integrationUC *biz.IntegrationUseCase
42+
ociUC *biz.OCIRepositoryUseCase
43+
credentialsProvider credentials.ReaderWriter
44+
log *log.Helper
45+
}
46+
47+
const Kind = "Dependency-Track"
48+
49+
func New(integrationUC *biz.IntegrationUseCase, ociUC *biz.OCIRepositoryUseCase, creds credentials.ReaderWriter, l log.Logger) *Integration {
50+
return &Integration{integrationUC, ociUC, creds, servicelogger.ScopedHelper(l, "biz/integration/deptrack")}
51+
}
52+
53+
func (uc *Integration) Add(ctx context.Context, orgID, host, apiKey string, enableProjectCreation bool) (*biz.Integration, error) {
54+
// Validate Credentials before saving them
55+
creds := &credentials.APICreds{Host: host, Key: apiKey}
56+
if err := creds.Validate(); err != nil {
57+
return nil, biz.NewErrValidation(err)
58+
}
59+
60+
// Create the secret in the external secrets manager
61+
secretID, err := uc.credentialsProvider.SaveCredentials(ctx, orgID, creds)
62+
if err != nil {
63+
return nil, fmt.Errorf("storing the credentials: %w", err)
64+
}
65+
66+
c := &v1.IntegrationConfig{
67+
Config: &v1.IntegrationConfig_DependencyTrack_{
68+
DependencyTrack: &v1.IntegrationConfig_DependencyTrack{
69+
AllowAutoCreate: enableProjectCreation, Domain: host,
70+
},
71+
},
72+
}
73+
74+
// Persist data
75+
return uc.integrationUC.Create(ctx, orgID, Kind, secretID, c)
76+
}
77+
78+
// Upload the SBOMs wrapped in the DSSE envelope to the configured Dependency Track instance
79+
func (uc *Integration) UploadSBOMs(envelope *dsse.Envelope, orgID, workflowID string) error {
80+
ctx := context.Background()
81+
uc.log.Infow("msg", "looking for integration", "workflowID", workflowID, "integration", Kind)
82+
83+
// List enabled integrations with this workflow
84+
attachments, err := uc.integrationUC.ListAttachments(ctx, orgID, workflowID)
85+
if err != nil {
86+
return err
87+
}
88+
89+
// Load the ones about dependency track
90+
var depTrackIntegrations []*biz.IntegrationAndAttachment
91+
for _, at := range attachments {
92+
integration, err := uc.integrationUC.FindByIDInOrg(ctx, orgID, at.IntegrationID.String())
93+
if err != nil {
94+
return err
95+
} else if integration == nil {
96+
continue
97+
}
98+
if integration.Kind == Kind {
99+
depTrackIntegrations = append(depTrackIntegrations, &biz.IntegrationAndAttachment{Integration: integration, IntegrationAttachment: at})
100+
}
101+
}
102+
103+
if len(depTrackIntegrations) == 0 {
104+
uc.log.Infow("msg", "no attached integrations", "workflowID", workflowID, "integration", Kind)
105+
return nil
106+
}
107+
108+
predicate, err := renderer.ExtractPredicate(envelope)
109+
if err != nil {
110+
return err
111+
}
112+
113+
repo, err := uc.ociUC.FindMainRepo(ctx, orgID)
114+
if err != nil {
115+
return err
116+
} else if repo == nil {
117+
return errors.NotFound("not found", "main repository not found")
118+
}
119+
120+
backend, err := oci.NewBackendProvider(uc.credentialsProvider).FromCredentials(ctx, repo.SecretName)
121+
if err != nil {
122+
return err
123+
}
124+
125+
for _, m := range predicate.Materials {
126+
if m.Type != contractAPI.CraftingSchema_Material_SBOM_CYCLONEDX_JSON.String() {
127+
continue
128+
}
129+
130+
buf := bytes.NewBuffer(nil)
131+
digest, ok := m.Material.SLSA.Digest["sha256"]
132+
if !ok {
133+
continue
134+
}
135+
136+
uc.log.Infow("msg", "SBOM present, downloading", "workflowID", workflowID, "integration", Kind, "name", m.Name)
137+
// Download SBOM
138+
if err := backend.Download(ctx, buf, digest); err != nil {
139+
return err
140+
}
141+
uc.log.Infow("msg", "SBOM downloaded", "digest", digest, "workflowID", workflowID, "integration", Kind, "name", m.Name)
142+
143+
// Run integrations with that sbom
144+
var wg sync.WaitGroup
145+
var errs = make(chan error)
146+
var wgDone = make(chan bool)
147+
148+
for _, i := range depTrackIntegrations {
149+
wg.Add(1)
150+
b := backoff.NewExponentialBackOff()
151+
b.MaxElapsedTime = 10 * time.Second
152+
153+
go func(i *biz.IntegrationAndAttachment) {
154+
defer wg.Done()
155+
err := backoff.RetryNotify(
156+
func() error {
157+
return doSendToDependencyTrack(ctx, uc.credentialsProvider, workflowID, buf, i, uc.log)
158+
},
159+
b,
160+
func(err error, delay time.Duration) {
161+
uc.log.Warnw("msg", "error uploading SBOM", "retry", delay, "error", err)
162+
},
163+
)
164+
if err != nil {
165+
errs <- err
166+
log.Error(err)
167+
}
168+
}(i)
169+
}
170+
171+
go func() {
172+
wg.Wait()
173+
close(wgDone)
174+
}()
175+
176+
select {
177+
case <-wgDone:
178+
break
179+
case err := <-errs:
180+
return err
181+
}
182+
}
183+
184+
return nil
185+
}
186+
187+
func doSendToDependencyTrack(ctx context.Context, credsReader credentials.Reader, workflowID string, sbom io.Reader, i *biz.IntegrationAndAttachment, log *log.Helper) error {
188+
integrationConfig := i.Integration.Config.GetDependencyTrack()
189+
attachmentConfig := i.IntegrationAttachment.Config.GetDependencyTrack()
190+
191+
creds := &credentials.APICreds{}
192+
if err := credsReader.ReadCredentials(ctx, i.SecretName, creds); err != nil {
193+
return err
194+
}
195+
196+
log.Infow("msg", "Sending SBOM to Dependency-Track",
197+
"host", integrationConfig.Domain,
198+
"projectID", attachmentConfig.GetProjectId(), "projectName", attachmentConfig.GetProjectName(),
199+
"workflowID", workflowID, "integration", Kind,
200+
)
201+
202+
d, err := dependencytrack.NewSBOMUploader(integrationConfig.Domain, creds.Key, sbom, attachmentConfig.GetProjectId(), attachmentConfig.GetProjectName())
203+
if err != nil {
204+
return err
205+
}
206+
207+
if err := d.Validate(ctx); err != nil {
208+
return err
209+
}
210+
211+
if err := d.Do(ctx); err != nil {
212+
return err
213+
}
214+
215+
log.Infow("msg", "SBOM Sent to Dependency-Track",
216+
"host", integrationConfig.Domain,
217+
"projectID", attachmentConfig.GetProjectId(), "projectName", attachmentConfig.GetProjectName(),
218+
"workflowID", workflowID, "integration", Kind,
219+
)
220+
221+
return nil
222+
}

0 commit comments

Comments
 (0)