Skip to content
Merged
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
2 changes: 2 additions & 0 deletions app/controlplane/cmd/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package main

import (
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/integration"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/data"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/server"
Expand All @@ -40,6 +41,7 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger) (*app, func(
server.ProviderSet,
data.ProviderSet,
biz.ProviderSet,
integration.ProviderSet,
service.ProviderSet,
wire.Bind(new(backend.Provider), new(*oci.BackendProvider)),
wire.Bind(new(biz.CASClient), new(*biz.CASClientUseCase)),
Expand Down
5 changes: 4 additions & 1 deletion app/controlplane/cmd/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions app/controlplane/internal/biz/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ func IsErrInvalidUUID(err error) bool {
return errors.As(err, &ErrInvalidUUID{})
}

type errValidation struct {
type ErrValidation struct {
err error
}

func newErrValidation(err error) errValidation {
return errValidation{err}
func NewErrValidation(err error) ErrValidation {
return ErrValidation{err}
}

func (e errValidation) Error() string {
func (e ErrValidation) Error() string {
return fmt.Sprintf("validation error: %s", e.err.Error())
}

func IsErrValidation(err error) bool {
return errors.As(err, &errValidation{})
return errors.As(err, &ErrValidation{})
}
32 changes: 5 additions & 27 deletions app/controlplane/internal/biz/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ type IntegrationUseCase struct {
logger *log.Helper
}

const DependencyTrackKind = "Dependency-Track"

type NewIntegrationUseCaseOpts struct {
IRepo IntegrationRepo
IaRepo IntegrationAttachmentRepo
Expand All @@ -91,34 +89,14 @@ func NewIntegrationUseCase(opts *NewIntegrationUseCaseOpts) *IntegrationUseCase
return &IntegrationUseCase{opts.IRepo, opts.IaRepo, opts.WfRepo, opts.CredsRW, servicelogger.ScopedHelper(opts.Logger, "biz/integration")}
}

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

// Validate Credentials before saving them
creds := &credentials.APICreds{Host: host, Key: apiKey}
if err := creds.Validate(); err != nil {
return nil, newErrValidation(err)
}

// Create the secret in the external secrets manager
secretID, err := uc.credsRW.SaveCredentials(ctx, orgID, creds)
if err != nil {
return nil, fmt.Errorf("storing the credentials: %w", err)
}

c := &v1.IntegrationConfig{
Config: &v1.IntegrationConfig_DependencyTrack_{
DependencyTrack: &v1.IntegrationConfig_DependencyTrack{
AllowAutoCreate: enableProjectCreation, Domain: host,
},
},
}

// Persist data
return uc.integrationRepo.Create(ctx, orgUUID, DependencyTrackKind, secretID, c)
return uc.integrationRepo.Create(ctx, orgUUID, kind, secretID, config)
}

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

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

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

if err := creds.Validate(); err != nil {
return newErrValidation(err)
return NewErrValidation(err)
}

// Instantiate an actual uploader to see if it would work with the current configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
//
// Copyright 2023 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 dependencytrack

import (
"bytes"
"context"
"fmt"
"io"
"sync"
"time"

"github.com/cenkalti/backoff/v4"
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/integrations/dependencytrack"
"github.com/chainloop-dev/chainloop/internal/attestation/renderer"
"github.com/chainloop-dev/chainloop/internal/blobmanager/oci"
"github.com/chainloop-dev/chainloop/internal/credentials"
"github.com/chainloop-dev/chainloop/internal/servicelogger"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-openapi/errors"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)

type Integration struct {
integrationUC *biz.IntegrationUseCase
ociUC *biz.OCIRepositoryUseCase
credentialsProvider credentials.ReaderWriter
log *log.Helper
}

const Kind = "Dependency-Track"

func New(integrationUC *biz.IntegrationUseCase, ociUC *biz.OCIRepositoryUseCase, creds credentials.ReaderWriter, l log.Logger) *Integration {
return &Integration{integrationUC, ociUC, creds, servicelogger.ScopedHelper(l, "biz/integration/deptrack")}
}

func (uc *Integration) Add(ctx context.Context, orgID, host, apiKey string, enableProjectCreation bool) (*biz.Integration, error) {
// Validate Credentials before saving them
creds := &credentials.APICreds{Host: host, Key: apiKey}
if err := creds.Validate(); err != nil {
return nil, biz.NewErrValidation(err)
}

// Create the secret in the external secrets manager
secretID, err := uc.credentialsProvider.SaveCredentials(ctx, orgID, creds)
if err != nil {
return nil, fmt.Errorf("storing the credentials: %w", err)
}

c := &v1.IntegrationConfig{
Config: &v1.IntegrationConfig_DependencyTrack_{
DependencyTrack: &v1.IntegrationConfig_DependencyTrack{
AllowAutoCreate: enableProjectCreation, Domain: host,
},
},
}

// Persist data
return uc.integrationUC.Create(ctx, orgID, Kind, secretID, c)
}

// Upload the SBOMs wrapped in the DSSE envelope to the configured Dependency Track instance
func (uc *Integration) UploadSBOMs(envelope *dsse.Envelope, orgID, workflowID string) error {
ctx := context.Background()
uc.log.Infow("msg", "looking for integration", "workflowID", workflowID, "integration", Kind)

// List enabled integrations with this workflow
attachments, err := uc.integrationUC.ListAttachments(ctx, orgID, workflowID)
if err != nil {
return err
}

// Load the ones about dependency track
var depTrackIntegrations []*biz.IntegrationAndAttachment
for _, at := range attachments {
integration, err := uc.integrationUC.FindByIDInOrg(ctx, orgID, at.IntegrationID.String())
if err != nil {
return err
} else if integration == nil {
continue
}
if integration.Kind == Kind {
depTrackIntegrations = append(depTrackIntegrations, &biz.IntegrationAndAttachment{Integration: integration, IntegrationAttachment: at})
}
}

if len(depTrackIntegrations) == 0 {
uc.log.Infow("msg", "no attached integrations", "workflowID", workflowID, "integration", Kind)
return nil
}

predicate, err := renderer.ExtractPredicate(envelope)
if err != nil {
return err
}

repo, err := uc.ociUC.FindMainRepo(ctx, orgID)
if err != nil {
return err
} else if repo == nil {
return errors.NotFound("not found", "main repository not found")
}

backend, err := oci.NewBackendProvider(uc.credentialsProvider).FromCredentials(ctx, repo.SecretName)
if err != nil {
return err
}

for _, m := range predicate.Materials {
if m.Type != contractAPI.CraftingSchema_Material_SBOM_CYCLONEDX_JSON.String() {
continue
}

buf := bytes.NewBuffer(nil)
digest, ok := m.Material.SLSA.Digest["sha256"]
if !ok {
continue
}

uc.log.Infow("msg", "SBOM present, downloading", "workflowID", workflowID, "integration", Kind, "name", m.Name)
// Download SBOM
if err := backend.Download(ctx, buf, digest); err != nil {
return err
}
uc.log.Infow("msg", "SBOM downloaded", "digest", digest, "workflowID", workflowID, "integration", Kind, "name", m.Name)

// Run integrations with that sbom
var wg sync.WaitGroup
var errs = make(chan error)
var wgDone = make(chan bool)

for _, i := range depTrackIntegrations {
wg.Add(1)
b := backoff.NewExponentialBackOff()
b.MaxElapsedTime = 10 * time.Second

go func(i *biz.IntegrationAndAttachment) {
defer wg.Done()
err := backoff.RetryNotify(
func() error {
return doSendToDependencyTrack(ctx, uc.credentialsProvider, workflowID, buf, i, uc.log)
},
b,
func(err error, delay time.Duration) {
uc.log.Warnw("msg", "error uploading SBOM", "retry", delay, "error", err)
},
)
if err != nil {
errs <- err
log.Error(err)
}
}(i)
}

go func() {
wg.Wait()
close(wgDone)
}()

select {
case <-wgDone:
break
case err := <-errs:
return err
}
}

return nil
}

func doSendToDependencyTrack(ctx context.Context, credsReader credentials.Reader, workflowID string, sbom io.Reader, i *biz.IntegrationAndAttachment, log *log.Helper) error {
integrationConfig := i.Integration.Config.GetDependencyTrack()
attachmentConfig := i.IntegrationAttachment.Config.GetDependencyTrack()

creds := &credentials.APICreds{}
if err := credsReader.ReadCredentials(ctx, i.SecretName, creds); err != nil {
return err
}

log.Infow("msg", "Sending SBOM to Dependency-Track",
"host", integrationConfig.Domain,
"projectID", attachmentConfig.GetProjectId(), "projectName", attachmentConfig.GetProjectName(),
"workflowID", workflowID, "integration", Kind,
)

d, err := dependencytrack.NewSBOMUploader(integrationConfig.Domain, creds.Key, sbom, attachmentConfig.GetProjectId(), attachmentConfig.GetProjectName())
if err != nil {
return err
}

if err := d.Validate(ctx); err != nil {
return err
}

if err := d.Do(ctx); err != nil {
return err
}

log.Infow("msg", "SBOM Sent to Dependency-Track",
"host", integrationConfig.Domain,
"projectID", attachmentConfig.GetProjectId(), "projectName", attachmentConfig.GetProjectName(),
"workflowID", workflowID, "integration", Kind,
)

return nil
}
Loading