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
142 changes: 136 additions & 6 deletions cmd/werf/sbom/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/werf/werf/v2/pkg/giterminism_manager"
"github.com/werf/werf/v2/pkg/sbom/extract"
sbomImage "github.com/werf/werf/v2/pkg/sbom/image"
"github.com/werf/werf/v2/pkg/storage"
"github.com/werf/werf/v2/pkg/tmp_manager"
"github.com/werf/werf/v2/pkg/true_git"
"github.com/werf/werf/v2/pkg/werf/global_warnings"
Expand All @@ -29,6 +30,9 @@ var commonCmdData common.CmdData
func NewCmd(ctx context.Context) *cobra.Command {
ctx = common.NewContextWithCmdData(ctx, &commonCmdData)

var tagFlag string
var digestFlag string

cmd := common.SetCommandContext(ctx, &cobra.Command{
Use: "get [IMAGE_NAME]",
Short: "Get SBOM of an image",
Expand All @@ -47,9 +51,22 @@ func NewCmd(ctx context.Context) *cobra.Command {
return err
}

if tagFlag != "" && digestFlag != "" {
common.PrintHelp(cmd)
return fmt.Errorf("--tag and --digest are mutually exclusive")
}

common.LogVersion()

return common.LogRunningTime(func() error {
if tagFlag != "" {
return runGetByTag(ctx, tagFlag)
}

if digestFlag != "" {
return runGetByDigest(ctx, digestFlag)
}

return runGet(ctx, args[0])
})
},
Expand Down Expand Up @@ -107,9 +124,127 @@ func NewCmd(ctx context.Context) *cobra.Command {

lo.Must0(common.SetupKubeConnectionFlags(&commonCmdData, cmd))

cmd.Flags().StringVarP(&tagFlag, "tag", "", "", "Content-based tag of the image to get SBOM for (mutually exclusive with --digest)")
cmd.Flags().StringVarP(&digestFlag, "digest", "", "", "Digest of the image to get SBOM for (mutually exclusive with --tag)")

return cmd
}

func runGetByTag(ctx context.Context, tag string) error {
global_warnings.PostponeMultiwerfNotUpToDateWarning(ctx)

commonManager, ctx, err := common.InitCommonComponents(ctx, common.InitCommonComponentsOptions{
Cmd: &commonCmdData,
InitWerf: true,
InitProcessContainerBackend: true,
InitDockerRegistry: true,
})
if err != nil {
return fmt.Errorf("component init error: %w", err)
}

defer func() {
if err := tmp_manager.DelegateCleanup(ctx); err != nil {
logboek.Context(ctx).Warn().LogF("Temporary files cleanup preparation failed: %s\n", err)
}
}()

repoAddr, err := commonCmdData.Repo.GetAddress()
if err != nil || repoAddr == storage.LocalStorageAddress {
return fmt.Errorf("--repo is required when using --tag")
}

sbomRef := sbomImage.BaseImageName(repoAddr, tag)
containerBackend := commonManager.ContainerBackend()

sbomJSON, err := getRawSbom(ctx, containerBackend, repoAddr, sbomRef)
if err != nil {
return err
}

return writeSbomToStdout(sbomJSON)
}

func runGetByDigest(ctx context.Context, imageDigest string) error {
global_warnings.PostponeMultiwerfNotUpToDateWarning(ctx)

_, ctx, err := common.InitCommonComponents(ctx, common.InitCommonComponentsOptions{
Cmd: &commonCmdData,
InitWerf: true,
InitDockerRegistry: true,
})
if err != nil {
return fmt.Errorf("component init error: %w", err)
}

defer func() {
if err := tmp_manager.DelegateCleanup(ctx); err != nil {
logboek.Context(ctx).Warn().LogF("Temporary files cleanup preparation failed: %s\n", err)
}
}()

repoAddr, err := commonCmdData.Repo.GetAddress()
if err != nil || repoAddr == storage.LocalStorageAddress {
return fmt.Errorf("--repo is required when using --digest")
}

registry, err := common.CreateDockerRegistry(ctx, repoAddr, *commonCmdData.InsecureRegistry, *commonCmdData.SkipTlsVerifyRegistry)
if err != nil {
return fmt.Errorf("create docker registry: %w", err)
}

digestToTag, err := sbomImage.BuildDigestToTagIndex(ctx, registry, repoAddr)
if err != nil {
return fmt.Errorf("build digest-to-tag index: %w", err)
}

sbomRef, err := sbomImage.ResolveSBOMReference(repoAddr, imageDigest, digestToTag)
if err != nil {
return err
}

sbomJSON, err := sbomImage.PullRawSbom(ctx, registry, sbomRef)
if err != nil {
return fmt.Errorf("pull SBOM image: %w", err)
}

return writeSbomToStdout(sbomJSON)
}

func getRawSbom(ctx context.Context, containerBackend container_backend.ContainerBackend, repoAddr, sbomRef string) ([]byte, error) {
opener := func() (io.ReadCloser, error) {
return containerBackend.SaveImageToStream(ctx, sbomRef)
}

sbomJSON, err := extract.FromImageBytes(opener)
if err == nil {
return sbomJSON, nil
}

logboek.Context(ctx).Info().LogF("SBOM image not found in local cache, pulling from registry\n")

registry, err := common.CreateDockerRegistry(ctx, repoAddr, *commonCmdData.InsecureRegistry, *commonCmdData.SkipTlsVerifyRegistry)
if err != nil {
return nil, fmt.Errorf("create docker registry: %w", err)
}

sbomJSON, err = sbomImage.PullRawSbom(ctx, registry, sbomRef)
if err != nil {
return nil, fmt.Errorf("pull SBOM image: %w", err)
}

return sbomJSON, nil
}

func writeSbomToStdout(data []byte) error {
return logboek.Streams().DoErrorWithoutProxyStreamDataFormatting(func() error {
if _, err := io.Copy(os.Stdout, bytes.NewReader(data)); err != nil {
return fmt.Errorf("write SBOM to stdout: %w", err)
}
return nil
})
}

func runGet(ctx context.Context, requestedImageName string) error {
global_warnings.PostponeMultiwerfNotUpToDateWarning(ctx)

Expand Down Expand Up @@ -234,12 +369,7 @@ func run(ctx context.Context, containerBackend container_backend.ContainerBacken
return fmt.Errorf("unable to find artifact file: %w", err)
}

return logboek.Streams().DoErrorWithoutProxyStreamDataFormatting(func() error {
if _, err = io.Copy(os.Stdout, bytes.NewReader(artifactContent)); err != nil {
return fmt.Errorf("unable to redirect artifact file content into stdout: %w", err)
}
return nil
})
return writeSbomToStdout(artifactContent)
}

func getSbomImageName(exportedImages []*image.Image, requestedImageName string) (string, error) {
Expand Down
4 changes: 4 additions & 0 deletions docs/_includes/reference/cli/werf_sbom_get.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ werf sbom get [IMAGE_NAME] [options]
multiple).
Also, can be specified with $WERF_DEV_IGNORE_* (e.g. $WERF_DEV_IGNORE_TESTS=*_test.go,
$WERF_DEV_IGNORE_DOCS=path/to/docs)
--digest=""
Digest of the image to get SBOM for (mutually exclusive with --tag)
--dir=""
Use specified project directory where project’s werf.yaml and other configuration files
should reside (default $WERF_DIR or current working directory)
Expand Down Expand Up @@ -285,6 +287,8 @@ werf sbom get [IMAGE_NAME] [options]

The same address should be specified for all werf processes that work with a single
repo. :local address allows execution of werf processes from a single host only
--tag=""
Content-based tag of the image to get SBOM for (mutually exclusive with --digest)
--tmp-dir=""
Use specified dir to store tmp files and dirs (default $WERF_TMP_DIR or system tmp dir)
--virtual-merge=false
Expand Down
55 changes: 54 additions & 1 deletion pkg/sbom/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func BaseImageName(repo, tag string) string {
return ImageName(fmt.Sprintf("%s:%s", repo, tag))
}

func PullCycloneDX16BOM(ctx context.Context, registry docker_registry.Interface, reference string) (*cdx.BOM, error) {
func PullRawSbom(ctx context.Context, registry docker_registry.Interface, reference string) ([]byte, error) {
var buf bytes.Buffer
if err := registry.PullImageArchive(ctx, &buf, reference); err != nil {
return nil, fmt.Errorf("pull image archive: %w", err)
Expand All @@ -68,10 +68,63 @@ func PullCycloneDX16BOM(ctx context.Context, registry docker_registry.Interface,
return nil, fmt.Errorf("extract SBOM from image: %w", err)
}

return sbomJSON, nil
}

func PullCycloneDX16BOM(ctx context.Context, registry docker_registry.Interface, reference string) (*cdx.BOM, error) {
sbomJSON, err := PullRawSbom(ctx, registry, reference)
if err != nil {
return nil, err
}

bom, err := cyclonedxutil.BuildCycloneDX16BOMFromJSON(sbomJSON)
if err != nil {
return nil, fmt.Errorf("parse CycloneDX BOM: %w", err)
}

return bom, nil
}

func BuildDigestToTagIndex(ctx context.Context, registry docker_registry.Interface, repo string) (map[string]string, error) {
tags, err := registry.Tags(ctx, repo)
if err != nil {
return nil, fmt.Errorf("list tags for %s: %w", repo, err)
}

result := make(map[string]string, len(tags))
for _, tag := range tags {
if strings.HasSuffix(tag, TagSuffix) {
continue
}

ref := fmt.Sprintf("%s:%s", repo, tag)

info, err := registry.TryGetRepoImage(ctx, ref)
if err != nil {
return nil, fmt.Errorf("get image info for %s: %w", ref, err)
}
if info == nil {
continue
}

d := info.GetDigest()
if d == "" {
continue
}

if _, exists := result[d]; !exists {
result[d] = tag
}
}

return result, nil
}

func ResolveSBOMReference(repo, imageDigest string, digestToTag map[string]string) (string, error) {
tag, ok := digestToTag[imageDigest]
if !ok {
return "", fmt.Errorf("no tag found for digest %s in repo %s", imageDigest, repo)
}

return BaseImageName(repo, tag), nil
}
52 changes: 4 additions & 48 deletions pkg/sbom/merge/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/werf/werf/v2/pkg/docker_registry"
"github.com/werf/werf/v2/pkg/sbom/convert"
"github.com/werf/werf/v2/pkg/sbom/cyclonedxutil"
"github.com/werf/werf/v2/pkg/sbom/image"
sbomImage "github.com/werf/werf/v2/pkg/sbom/image"
)

type Options struct {
Expand Down Expand Up @@ -179,7 +179,7 @@ func PullAndParseImages(ctx context.Context, registry docker_registry.Interface,
var digestToTag map[string]string
err := logboek.Context(ctx).Default().LogProcess("Building digest-to-tag index").DoError(func() error {
var err error
digestToTag, err = buildDigestToTagIndex(ctx, registry, repo)
digestToTag, err = sbomImage.BuildDigestToTagIndex(ctx, registry, repo)
if err != nil {
return err
}
Expand All @@ -199,14 +199,14 @@ func PullAndParseImages(ctx context.Context, registry docker_registry.Interface,
err := logboek.Context(ctx).Default().
LogProcess("[%d/%d] %s", idx, total, imageName).
DoError(func() error {
sbomRef, err := resolveSBOMReference(repo, imageDigest, digestToTag)
sbomRef, err := sbomImage.ResolveSBOMReference(repo, imageDigest, digestToTag)
if err != nil {
return fmt.Errorf("resolve SBOM reference for %q: %w", imageName, err)
}

logboek.Context(ctx).Default().LogFDetails("sbom reference: %s\n", sbomRef)

bom, err := image.PullCycloneDX16BOM(ctx, registry, sbomRef)
bom, err := sbomImage.PullCycloneDX16BOM(ctx, registry, sbomRef)
if err != nil {
return fmt.Errorf("unable to pull SBOM for %q (%s): %w", imageName, sbomRef, err)
}
Expand All @@ -227,50 +227,6 @@ func PullAndParseImages(ctx context.Context, registry docker_registry.Interface,
return images, nil
}

func resolveSBOMReference(repo, imageDigest string, digestToTag map[string]string) (string, error) {
tag, ok := digestToTag[imageDigest]
if !ok {
return "", fmt.Errorf("no tag found for digest %s in repo %s", imageDigest, repo)
}

return image.BaseImageName(repo, tag), nil
}

func buildDigestToTagIndex(ctx context.Context, registry docker_registry.Interface, repo string) (map[string]string, error) {
tags, err := registry.Tags(ctx, repo)
if err != nil {
return nil, fmt.Errorf("list tags for %s: %w", repo, err)
}

result := make(map[string]string, len(tags))
for _, tag := range tags {
if strings.HasSuffix(tag, "-sbom") {
continue
}

ref := fmt.Sprintf("%s:%s", repo, tag)

info, err := registry.TryGetRepoImage(ctx, ref)
if err != nil {
return nil, fmt.Errorf("get image info for %s: %w", ref, err)
}
if info == nil {
continue
}

d := info.GetDigest()
if d == "" {
continue
}

if _, exists := result[d]; !exists {
result[d] = tag
}
}

return result, nil
}

func WriteOutput(data []byte, outputPath string) error {
if outputPath == "" {
return logboek.Streams().DoErrorWithoutProxyStreamDataFormatting(func() error {
Expand Down
Loading
Loading