diff --git a/go.mod b/go.mod index 39d481b..a4bb2cd 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/anchore/stereoscope v0.0.0-20221006201143-d24c9d626b33 github.com/anchore/syft v0.62.1 github.com/aquasecurity/trivy v0.30.4 - github.com/atomist-skills/go-skill v0.0.6-0.20221003172518-c3d268e1f3f1 + github.com/atomist-skills/go-skill v0.0.6-0.20221221214636-a7de163fd901 github.com/briandowns/spinner v1.12.0 github.com/docker/cli v20.10.21+incompatible github.com/docker/docker v20.10.17+incompatible diff --git a/go.sum b/go.sum index f2503ac..bf7b894 100644 --- a/go.sum +++ b/go.sum @@ -209,6 +209,8 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/atomist-skills/go-skill v0.0.6-0.20221003172518-c3d268e1f3f1 h1:EzSOh9LLtL/3IzbPUFSp/6OF4DrgiCPxCC3x3jjD9Bs= github.com/atomist-skills/go-skill v0.0.6-0.20221003172518-c3d268e1f3f1/go.mod h1:DRmwrZL5kG68Mn8VDw/Xr7rDhyl+laD7NFHIrQr54yo= +github.com/atomist-skills/go-skill v0.0.6-0.20221221214636-a7de163fd901 h1:0fqUAo4MmWXnWIDCG7JBe903M3WJ+tqQetPIkVfcXKo= +github.com/atomist-skills/go-skill v0.0.6-0.20221221214636-a7de163fd901/go.mod h1:DRmwrZL5kG68Mn8VDw/Xr7rDhyl+laD7NFHIrQr54yo= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.44.46 h1:BsKENvu24eXg7CWQ2wJAjKbDFkGP+hBtxKJIR3UdcB8= github.com/aws/aws-sdk-go v1.44.46/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= diff --git a/registry/read.go b/registry/read.go index 7ca6f32..fa59e71 100644 --- a/registry/read.go +++ b/registry/read.go @@ -17,6 +17,8 @@ package registry import ( + stereoscopeimage "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/syft/source" "github.com/atomist-skills/go-skill" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/pkg/errors" @@ -31,15 +33,31 @@ func ReadImage(name string, path string) (*ImageCache, error) { mani, err := index.IndexManifest() hash := mani.Manifests[0].Digest img, _ := index.Image(hash) + + skill.Log.Debugf("Parsing image") + input := source.Input{ + Scheme: source.ImageScheme, + ImageSource: stereoscopeimage.OciDirectorySource, + Location: path, + } + src, cleanup, err := source.New(input, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to create new source") + } + skill.Log.Debugf("Parse image") skill.Log.Infof("Loaded image") + return &ImageCache{ Id: hash.String(), Digest: hash.String(), Tags: []string{}, Name: name, Image: &img, + Source: src, ImagePath: path, Ref: nil, - copy: false, + + copy: false, + sourceCleanup: cleanup, }, nil } diff --git a/registry/save.go b/registry/save.go index 61a99db..57e2c1a 100644 --- a/registry/save.go +++ b/registry/save.go @@ -19,10 +19,13 @@ package registry import ( "context" "fmt" + "io" "os" "path/filepath" "strings" + stereoscopeimage "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/syft/source" "github.com/atomist-skills/go-skill" "github.com/docker/cli/cli/command" "github.com/docker/index-cli-plugin/internal" @@ -70,11 +73,14 @@ type ImageCache struct { Tags []string Image *v1.Image + Source *source.Source ImagePath string Ref *name.Reference - copy bool - cli command.Cli + remote bool + copy bool + cli command.Cli + sourceCleanup func() } func (c *ImageCache) StoreImage() error { @@ -82,6 +88,7 @@ func (c *ImageCache) StoreImage() error { return nil } skill.Log.Debugf("Copying image to %s", c.ImagePath) + var imageSource stereoscopeimage.Source if format := os.Getenv("ATOMIST_CACHE_FORMAT"); format == "" || format == "oci" { spinner := internal.StartSpinner("info", "Copying image", c.cli.Out().IsTerminal()) @@ -96,53 +103,113 @@ func (c *ImageCache) StoreImage() error { if err = p.AppendImage(*c.Image); err != nil { return err } + + imageSource = stereoscopeimage.OciDirectorySource + spinner.Stop() - skill.Log.Infof("Copied image") - return nil } else if format == "tar" { - u := make(chan v1.Update, 0) - errchan := make(chan error) - go func() { - if err := tarball.WriteToFile(c.ImagePath, *c.Ref, *c.Image, tarball.WithProgress(u)); err != nil { - errchan <- errors.Wrapf(err, "failed to write tmp image archive") - } - errchan <- nil - }() + if c.remote { + u := make(chan v1.Update, 0) + errchan := make(chan error) + go func() { + if err := tarball.WriteToFile(c.ImagePath, *c.Ref, *c.Image, tarball.WithProgress(u)); err != nil { + errchan <- errors.Wrapf(err, "failed to write tmp image archive") + } + errchan <- nil + }() - var update v1.Update - var err error - var pp int64 - spinner := internal.StartSpinner("info", "Copying image", c.cli.Out().IsTerminal()) - defer spinner.Stop() - for { - select { - case update = <-u: - if update.Total > 0 { - p := 100 * update.Complete / update.Total - if pp != p { - spinner.WithFields(internal.Fields{ - "event": "progress", - "total": update.Total, - "complete": update.Complete, - }).Update(fmt.Sprintf("Copying image %d%% %s/%s", p, humanize.Bytes(uint64(update.Complete)), humanize.Bytes(uint64(update.Total)))) - pp = p + var update v1.Update + var err error + var pp int64 + spinner := internal.StartSpinner("info", "Copying image", c.cli.Out().IsTerminal()) + defer spinner.Stop() + loop := true + for loop { + select { + case update = <-u: + if update.Total > 0 { + p := 100 * update.Complete / update.Total + if pp != p { + spinner.WithFields(internal.Fields{ + "event": "progress", + "total": update.Total, + "complete": update.Complete, + }).Update(fmt.Sprintf("Copying image %d%% %s/%s", p, humanize.Bytes(uint64(update.Complete)), humanize.Bytes(uint64(update.Total)))) + pp = p + } + } + case err = <-errchan: + if err != nil { + return err + } else { + spinner.Stop() + skill.Log.Infof("Copied image") + loop = false } } - case err = <-errchan: + } + + } else { + spinner := internal.StartSpinner("info", "Copying image", c.cli.Out().IsTerminal()) + defer spinner.Stop() + tempTarFile, err := os.Create(c.ImagePath) + if err != nil { + return errors.Wrap(err, "unable to create temp file for image") + } + defer func() { + err := tempTarFile.Close() + if err != nil { + skill.Log.Errorf("unable to close temp file (%s): %w", tempTarFile.Name(), err) + } + }() + + readCloser, err := c.cli.Client().ImageSave(context.Background(), []string{c.Id}) + if err != nil { + return errors.Wrap(err, "unable to save image tar") + } + defer func() { + err := readCloser.Close() if err != nil { - return err - } else { - spinner.Stop() - skill.Log.Infof("Copied image") - return nil + skill.Log.Errorf("unable to close temp file (%s): %w", tempTarFile.Name(), err) } + }() + + nBytes, err := io.Copy(tempTarFile, readCloser) + if err != nil { + return fmt.Errorf("unable to save image to tar: %w", err) + } + if nBytes == 0 { + return errors.New("cannot provide an empty image") } + spinner.Stop() } + + imageSource = stereoscopeimage.DockerTarballSource } + + skill.Log.Debugf("Parsing image") + input := source.Input{ + Scheme: source.ImageScheme, + ImageSource: imageSource, + Location: c.ImagePath, + } + src, cleanup, err := source.New(input, nil, nil) + if err != nil { + return errors.Wrap(err, "failed to create new image source") + } + c.Source = src + c.sourceCleanup = cleanup + + skill.Log.Debugf("Parsed image") + skill.Log.Infof("Copied image") + return nil } func (c *ImageCache) Cleanup() { + if c.sourceCleanup != nil { + c.sourceCleanup() + } if !c.copy { return } @@ -169,7 +236,7 @@ func SaveImage(image string, cli command.Cli) (*ImageCache, error) { } tarPath := filepath.Join(path, "sha256", digest[7:]) tarFileName := filepath.Join(tarPath, uuid.NewString()) - if os.Getenv("ATOMIST_CACHE_FORMAT") == "tar" { + if os.Getenv("ATOMIST_CACHE_FORMAT") != "oci" { tarFileName += ".tar" } @@ -204,6 +271,7 @@ func SaveImage(image string, cli command.Cli) (*ImageCache, error) { name = strings.Split(t, ":")[0] tags = append(tags, strings.Split(t, ":")[1]) } + return &ImageCache{ Id: im.ID, Digest: digest, @@ -214,6 +282,7 @@ func SaveImage(image string, cli command.Cli) (*ImageCache, error) { Ref: &ref, ImagePath: imagePath, copy: true, + remote: false, cli: cli, }, nil } @@ -250,6 +319,7 @@ func SaveImage(image string, cli command.Cli) (*ImageCache, error) { Ref: &ref, ImagePath: imagePath, copy: true, + remote: true, cli: cli, }, nil } diff --git a/sbom/detect/detect.go b/sbom/detect/detect.go index e8f3eb2..fbce8ef 100644 --- a/sbom/detect/detect.go +++ b/sbom/detect/detect.go @@ -28,7 +28,7 @@ import ( "github.com/docker/index-cli-plugin/types" ) -type PackageDetector = func(packages []types.Package, image source.Source, lm types.LayerMapping) []types.Package +type PackageDetector = func(packages []types.Package, image *source.Source, lm *types.LayerMapping) []types.Package var detectors []PackageDetector @@ -36,7 +36,7 @@ func init() { detectors = []PackageDetector{nodePackageDetector()} } -func AdditionalPackages(packages []types.Package, image source.Source, lm types.LayerMapping) []types.Package { +func AdditionalPackages(packages []types.Package, image *source.Source, lm *types.LayerMapping) []types.Package { additionalPackages := make([]types.Package, 0) for _, d := range detectors { additionalPackages = append(additionalPackages, d(packages, image, lm)...) @@ -45,7 +45,7 @@ func AdditionalPackages(packages []types.Package, image source.Source, lm types. } func stringsNodeDetector(executable string, versionEnvVar string, expr *regexp.Regexp, pkg types.Package, filterFunc func(purl string) bool) PackageDetector { - return func(packages []types.Package, image source.Source, lm types.LayerMapping) []types.Package { + return func(packages []types.Package, image *source.Source, lm *types.LayerMapping) []types.Package { // Already found via package manager for _, p := range packages { if filterFunc(p.Purl) { diff --git a/sbom/index.go b/sbom/index.go index ab0d575..54c285e 100644 --- a/sbom/index.go +++ b/sbom/index.go @@ -87,26 +87,28 @@ func indexImage(cache *registry.ImageCache, cli command.Cli) (*types.Sbom, error err := cache.StoreImage() defer cache.Cleanup() if err != nil { - return nil, errors.Wrapf(err, "failed to copy image") + return nil, errors.Wrap(err, "failed to copy image") } - lm := createLayerMapping(*cache.Image) - + lm, err := createLayerMapping(cache) + if err != nil { + return nil, errors.Wrap(err, "failed to index image") + } s := internal.StartSpinner("info", "Indexing", cli.Out().IsTerminal()) defer s.Stop() trivyResultChan := make(chan types.IndexResult) syftResultChan := make(chan types.IndexResult) - go trivySbom(cache.ImagePath, lm, trivyResultChan) - go syftSbom(cache.ImagePath, lm, syftResultChan) + go trivySbom(cache, lm, trivyResultChan) + go syftSbom(cache, lm, syftResultChan) trivyResult := <-trivyResultChan syftResult := <-syftResultChan if trivyResult.Error != nil { - return nil, errors.Wrapf(trivyResult.Error, "failed to index image") + return nil, errors.Wrap(trivyResult.Error, "failed to index image") } if syftResult.Error != nil { - return nil, errors.Wrapf(syftResult.Error, "failed to index image") + return nil, errors.Wrap(syftResult.Error, "failed to index image") } trivyResult.Packages, err = types.NormalizePackages(trivyResult.Packages) @@ -120,10 +122,14 @@ func indexImage(cache *registry.ImageCache, cli command.Cli) (*types.Sbom, error s.Stop() skill.Log.Infof(`Indexed %d packages`, len(packages)) - manifest, _ := (*cache.Image).RawManifest() - config, _ := (*cache.Image).RawConfigFile() - c, _ := (*cache.Image).ConfigFile() - m, _ := (*cache.Image).Manifest() + rawManifest := cache.Source.Image.Metadata.RawManifest + rawConfig := cache.Source.Image.Metadata.RawConfig + + var manifest v1.Manifest + err = json.Unmarshal(rawManifest, &manifest) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal manifest") + } sbom := types.Sbom{ Artifacts: packages, @@ -132,17 +138,17 @@ func indexImage(cache *registry.ImageCache, cli command.Cli) (*types.Sbom, error Image: types.ImageSource{ Name: cache.Name, Digest: cache.Digest, - Manifest: m, - Config: c, - RawManifest: base64.StdEncoding.EncodeToString(manifest), - RawConfig: base64.StdEncoding.EncodeToString(config), + Manifest: &manifest, + Config: &cache.Source.Image.Metadata.Config, + RawManifest: base64.StdEncoding.EncodeToString(rawManifest), + RawConfig: base64.StdEncoding.EncodeToString(rawConfig), Distro: syftResult.Distro, Platform: types.Platform{ - Os: c.OS, - Architecture: c.Architecture, - Variant: c.Variant, + Os: cache.Source.Image.Metadata.Config.OS, + Architecture: cache.Source.Image.Metadata.Config.Architecture, + Variant: cache.Source.Image.Metadata.Config.Variant, }, - Size: m.Config.Size, + Size: cache.Source.Image.Metadata.Size, }, }, Descriptor: types.Descriptor{ @@ -160,11 +166,11 @@ func indexImage(cache *registry.ImageCache, cli command.Cli) (*types.Sbom, error if err == nil { err = os.MkdirAll(filepath.Dir(sbomFilePath), os.ModePerm) if err != nil { - return nil, errors.Wrapf(err, "failed create to sbom folder") + return nil, errors.Wrap(err, "failed create to sbom folder") } err = os.WriteFile(sbomFilePath, js, 0644) if err != nil { - return nil, errors.Wrapf(err, "failed to write sbom") + return nil, errors.Wrap(err, "failed to write sbom") } } @@ -191,7 +197,8 @@ func cachedSbom(sbomFilePath string) *types.Sbom { return nil } -func createLayerMapping(img v1.Image) types.LayerMapping { +func createLayerMapping(cache *registry.ImageCache) (*types.LayerMapping, error) { + skill.Log.Debugf("Creating layer mapping") lm := types.LayerMapping{ ByDiffId: make(map[string]string, 0), ByDigest: make(map[string]string, 0), @@ -199,22 +206,21 @@ func createLayerMapping(img v1.Image) types.LayerMapping { DigestByOrdinal: make(map[int]string, 0), OrdinalByDiffId: make(map[string]int, 0), } - config, _ := img.ConfigFile() - diffIds := config.RootFS.DiffIDs - manifest, _ := img.Manifest() - layers := manifest.Layers + + diffIds := cache.Source.Image.Metadata.Config.RootFS.DiffIDs + layers := cache.Source.Metadata.ImageMetadata.Layers for i := range layers { layer := layers[i] diffId := diffIds[i] - lm.ByDiffId[diffId.String()] = layer.Digest.String() - lm.ByDigest[layer.Digest.String()] = diffId.String() + lm.ByDiffId[diffId.String()] = layer.Digest + lm.ByDigest[layer.Digest] = diffId.String() lm.OrdinalByDiffId[diffId.String()] = i lm.DiffIdByOrdinal[i] = diffId.String() - lm.DigestByOrdinal[i] = layer.Digest.String() + lm.DigestByOrdinal[i] = layer.Digest } skill.Log.Debugf("Created layer mapping") - return lm + return &lm, nil } diff --git a/sbom/lsp.go b/sbom/lsp.go new file mode 100644 index 0000000..e7f804a --- /dev/null +++ b/sbom/lsp.go @@ -0,0 +1,43 @@ +/* + * Copyright © 2022 Docker, Inc. + * + * 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 sbom + +import ( + "github.com/docker/cli/cli/command" + cliflags "github.com/docker/cli/cli/flags" + "github.com/pkg/errors" +) + +func Send(image string, tx chan<- string) error { + cmd, err := command.NewDockerCli() + if err != nil { + return errors.Wrap(err, "failed to create docker cli") + } + if err := cmd.Initialize(cliflags.NewClientOptions()); err != nil { + return errors.Wrap(err, "failed to initialize docker cli") + } + sbom, err := IndexImage(image, cmd) + if err != nil { + return errors.Wrap(err, "failed to create sbom") + } + err = sendSbom(sbom, tx) + if err != nil { + return errors.Wrap(err, "failed to send sbom") + } + close(tx) + return nil +} diff --git a/sbom/lsp_test.go b/sbom/lsp_test.go new file mode 100644 index 0000000..3173bf1 --- /dev/null +++ b/sbom/lsp_test.go @@ -0,0 +1,37 @@ +/* + * Copyright © 2022 Docker, Inc. + * + * 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 sbom + +import ( + "testing" +) + +func TestSend(t *testing.T) { + tx := make(chan string, 10) + transactions := make([]string, 0) + + err := Send("alpine@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c", tx) + if err != nil { + t.Fail() + } + for elem := range tx { + transactions = append(transactions, elem) + } + if len(transactions) != 3 { + t.Errorf("expected 3 transactions, instead got %d", len(transactions)) + } +} diff --git a/sbom/syft.go b/sbom/syft.go index 4d0822f..eb28149 100644 --- a/sbom/syft.go +++ b/sbom/syft.go @@ -30,6 +30,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/deb" "github.com/anchore/syft/syft/pkg/cataloger/rpm" "github.com/anchore/syft/syft/source" + "github.com/docker/index-cli-plugin/registry" "github.com/docker/index-cli-plugin/sbom/detect" "github.com/docker/index-cli-plugin/sbom/util" "github.com/docker/index-cli-plugin/types" @@ -38,7 +39,7 @@ import ( type packageMapping map[string]*stereoscopeimage.Layer -func syftSbom(ociPath string, lm types.LayerMapping, resultChan chan<- types.IndexResult) { +func syftSbom(cache *registry.ImageCache, lm *types.LayerMapping, resultChan chan<- types.IndexResult) { result := types.IndexResult{ Name: "syft", Status: types.Success, @@ -47,27 +48,7 @@ func syftSbom(ociPath string, lm types.LayerMapping, resultChan chan<- types.Ind defer close(resultChan) - im := stereoscopeimage.OciDirectorySource - if strings.HasSuffix(ociPath, ".tar") { - im = stereoscopeimage.DockerTarballSource - } - - i := source.Input{ - Scheme: source.ImageScheme, - ImageSource: im, - Location: ociPath, - } - src, cleanup, err := source.New(i, nil, nil) - if err != nil { - result.Status = types.Failed - result.Error = errors.Wrap(err, "failed to create image source") - resultChan <- result - return - - } - defer cleanup() - - packageCatalog, packageRelationships, distro, err := syft.CatalogPackages(src, cataloger.DefaultConfig()) + packageCatalog, packageRelationships, distro, err := syft.CatalogPackages(cache.Source, cataloger.DefaultConfig()) if err != nil { result.Status = types.Failed result.Error = errors.Wrap(err, "failed to index image") @@ -80,7 +61,7 @@ func syftSbom(ociPath string, lm types.LayerMapping, resultChan chan<- types.Ind result.Distro = d pm := make(packageMapping, 0) - for _, layer := range src.Image.Layers { + for _, layer := range cache.Source.Image.Layers { layerPkgs := make([]pkg2.Package, 0) res := util.NewSingleLayerResolver(layer) apkPkgs, _, err := apkdb.NewApkdbCataloger().Catalog(res) @@ -139,7 +120,7 @@ func syftSbom(ociPath string, lm types.LayerMapping, resultChan chan<- types.Ind result.Packages = append(result.Packages, pkg...) } - result.Packages = append(result.Packages, detect.AdditionalPackages(result.Packages, *src, lm)...) + result.Packages = append(result.Packages, detect.AdditionalPackages(result.Packages, cache.Source, lm)...) resultChan <- result } @@ -150,7 +131,7 @@ type sourcePackage struct { relationship string } -func toPackage(p pkg2.Package, rels []artifact.Relationship, qualifiers map[string]string, lm types.LayerMapping, pm packageMapping) []types.Package { +func toPackage(p pkg2.Package, rels []artifact.Relationship, qualifiers map[string]string, lm *types.LayerMapping, pm packageMapping) []types.Package { pkg := types.Package{ Purl: p.PURL, Licenses: p.Licenses, @@ -293,10 +274,11 @@ func toPackage(p pkg2.Package, rels []artifact.Relationship, qualifiers map[stri // fix up the package manager files for i, loc := range pkg.Locations { if loc.Path == "/lib/apk/db/installed" || loc.Path == "/var/lib/dpkg/status" || loc.Path == "/var/lib/rpm/Packages" { - layer := pm[toKey(p)] - // the stereoscope layers use diff_ids internally as their digest - pkg.Locations[i].DiffId = layer.Metadata.Digest - pkg.Locations[i].Digest = lm.ByDiffId[layer.Metadata.Digest] + if layer, ok := pm[toKey(p)]; ok { + // the stereoscope layers use diff_ids internally as their digest + pkg.Locations[i].DiffId = layer.Metadata.Digest + pkg.Locations[i].Digest = lm.ByDiffId[layer.Metadata.Digest] + } } } diff --git a/sbom/trivy.go b/sbom/trivy.go index 35274b5..dc153d4 100644 --- a/sbom/trivy.go +++ b/sbom/trivy.go @@ -32,11 +32,12 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/cache" "github.com/aquasecurity/trivy/pkg/fanal/image" "github.com/aquasecurity/trivy/pkg/fanal/utils" + "github.com/docker/index-cli-plugin/registry" "github.com/docker/index-cli-plugin/types" "github.com/pkg/errors" ) -func trivySbom(ociPath string, lm types.LayerMapping, resultChan chan<- types.IndexResult) { +func trivySbom(cache *registry.ImageCache, lm *types.LayerMapping, resultChan chan<- types.IndexResult) { result := types.IndexResult{ Name: "trivy", Status: types.Success, @@ -54,7 +55,7 @@ func trivySbom(ociPath string, lm types.LayerMapping, resultChan chan<- types.In } defer cacheClient.Close() - img, err := image.NewArchiveImage(ociPath) + img, err := image.NewArchiveImage(cache.ImagePath) if err != nil { result.Status = types.Failed result.Error = errors.Wrap(err, "failed to open archived image") diff --git a/sbom/upload.go b/sbom/upload.go index e9a51de..6888dcd 100644 --- a/sbom/upload.go +++ b/sbom/upload.go @@ -33,25 +33,67 @@ import ( "olympos.io/encoding/edn" ) +type TransactionMaker = func() skill.Transaction + // UploadSbom transact an image and its data into the data plane func UploadSbom(sb *types.Sbom, workspace string, apikey string) error { - host, name, err := parseReference(sb) + correlationId := uuid.NewString() + context := skill.RequestContext{ + Event: skill.EventIncoming{ + ExecutionId: correlationId, + WorkspaceId: workspace, + Token: apikey, + }, + } + + newTransaction := context.NewTransaction + image, err := transactSbom(sb, newTransaction) if err != nil { - return errors.Wrapf(err, "failed to obtain host and repository") + return errors.Wrap(err, "failed to transact image") } - config := (*sb).Source.Image.Config - manifest := (*sb).Source.Image.Manifest - now := time.Now() + imageName := "" + if image.Repository.Host != "hub.docker.com" { + imageName = image.Repository.Host + "/" + } + imageName += image.Repository.Name + + skill.Log.Infof("Inspect image at https://dso.docker.com/%s/overview/images/%s/digests/%s", workspace, imageName, image.Digest) + + return nil +} + +func sendSbom(sb *types.Sbom, entities chan<- string) error { correlationId := uuid.NewString() context := skill.RequestContext{ Event: skill.EventIncoming{ ExecutionId: correlationId, - WorkspaceId: workspace, - Token: apikey, }, } - transaction := context.NewTransaction().Ordered() + + newTransaction := func() skill.Transaction { + return context.NewTransactionWithTransactor(func(entitiesString string) { + entities <- entitiesString + }) + } + _, err := transactSbom(sb, newTransaction) + if err != nil { + return errors.Wrap(err, "failed to transact image") + } + + return nil +} + +func transactSbom(sb *types.Sbom, newTransaction func() skill.Transaction) (*ImageEntity, error) { + now := time.Now() + host, name, err := parseReference(sb) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain host and repository") + } + config := (*sb).Source.Image.Config + manifest := (*sb).Source.Image.Manifest + + transaction := newTransaction().Ordered() ports := parsePorts(config) env, envVars := parseEnvVars(config) sha := parseSha(config) @@ -111,7 +153,7 @@ func UploadSbom(sb *types.Sbom, workspace string, apikey string) error { image.Sha = sha } - if len(*sb.Source.Image.Tags) > 0 { + if sb.Source.Image.Tags != nil && len(*sb.Source.Image.Tags) > 0 { image.Tags = &skill.ManyRef{Add: *sb.Source.Image.Tags} for _, t := range *sb.Source.Image.Tags { @@ -136,13 +178,13 @@ func UploadSbom(sb *types.Sbom, workspace string, apikey string) error { // transact the image with all its metadata (repo, tags, layers, blobs, ports, env etc) err = transaction.AddEntities(image, platform).Transact() if err != nil { - return errors.Wrapf(err, "failed to transact image") + return nil, errors.Wrapf(err, "failed to transact image") } // transact all packages in chunks packageChunks := internal.ChunkSlice(sb.Artifacts, 20) for _, packages := range packageChunks { - transaction := context.NewTransaction().Ordered() + transaction := newTransaction().Ordered() image = ImageEntity{ Digest: sb.Source.Image.Digest, @@ -186,7 +228,7 @@ func UploadSbom(sb *types.Sbom, workspace string, apikey string) error { image.Dependencies = &skill.ManyRef{Add: transaction.EntityRefs("package/dependency")} err := transaction.AddEntities(image).Transact() if err != nil { - return errors.Wrapf(err, "failed to transact packages") + return nil, errors.Wrapf(err, "failed to transact packages") } } @@ -194,20 +236,11 @@ func UploadSbom(sb *types.Sbom, workspace string, apikey string) error { Digest: sb.Source.Image.Digest, SbomState: Indexed, } - err = context.NewTransaction().Ordered().AddEntities(image).Transact() + err = newTransaction().Ordered().AddEntities(image).Transact() if err != nil { - return errors.Wrapf(err, "failed to transact packages") + return nil, errors.Wrapf(err, "failed to transact packages") } - - imageName := "" - if host != "hub.docker.com" { - imageName = host + "/" - } - imageName += name - - skill.Log.Infof("Inspect image at https://dso.docker.com/%s/overview/images/%s/digests/%s", workspace, imageName, image.Digest) - - return nil + return &image, nil } func digestChainIds(manifest *v1.Manifest) []digest.Digest { diff --git a/types/types.go b/types/types.go index 75c6f34..c0526c4 100644 --- a/types/types.go +++ b/types/types.go @@ -107,8 +107,8 @@ type ImageSource struct { Name string `json:"name"` Digest string `json:"digest"` Tags *[]string `json:"tags,omitempty"` - Manifest *v1.Manifest `json:"manifest"` - Config *v1.ConfigFile `json:"config"` + Manifest *v1.Manifest `json:"manifest,omitempty"` + Config *v1.ConfigFile `json:"config,omitempty"` RawManifest string `json:"raw_manifest"` RawConfig string `json:"raw_config"` Distro Distro `json:"distro"`