Skip to content

Commit

Permalink
Make it work for CRI-O
Browse files Browse the repository at this point in the history
  • Loading branch information
lebauce committed Dec 23, 2024
1 parent 2680ce0 commit 94636e8
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 503 deletions.
2 changes: 1 addition & 1 deletion pkg/sbom/collectors/host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (c *Collector) Scan(ctx context.Context, request sbom.ScanRequest) sbom.Sca
}
log.Infof("host scan request [%v]", hostScanRequest.ID())

report, err := c.trivyCollector.ScanFilesystem(ctx, hostScanRequest.FS, hostScanRequest.Path, c.opts)
report, err := c.trivyCollector.ScanFilesystem(ctx, hostScanRequest.Path, c.opts)
return sbom.ScanResult{
Error: err,
Report: report,
Expand Down
31 changes: 3 additions & 28 deletions pkg/sbom/collectors/host/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
package host

import (
"io/fs"
"os"
"path/filepath"

"github.com/DataDog/datadog-agent/pkg/sbom/types"
)
Expand All @@ -17,34 +15,11 @@ import (
// hashable to be pushed in the work queue for processing.
type scanRequest struct {
Path string
FS fs.FS
}

type relFS struct {
root string
fs fs.FS
}

func newFS(root string) fs.FS {
fs := os.DirFS(root)
return &relFS{root: "/", fs: fs}
}

func (f *relFS) Open(name string) (fs.File, error) {
if filepath.IsAbs(name) {
var err error
name, err = filepath.Rel(f.root, name)
if err != nil {
return nil, err
}
}

return f.fs.Open(name)
}

// NewScanRequest creates a new scan request
func NewScanRequest(path string, fs fs.FS) types.ScanRequest {
return scanRequest{Path: path, FS: fs}
func NewScanRequest(path string) types.ScanRequest {
return scanRequest{Path: path}
}

// NewHostScanRequest creates a new scan request for the root filesystem
Expand All @@ -54,7 +29,7 @@ func NewHostScanRequest() types.ScanRequest {
// if hostRoot := os.Getenv("HOST_ROOT"); env.IsContainerized() && hostRoot != "" {
scanPath = hostRoot
}
return NewScanRequest("/", newFS(scanPath))
return NewScanRequest(scanPath)
}

// Collector returns the collector name
Expand Down
2 changes: 1 addition & 1 deletion pkg/security/resolvers/sbom/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func (r *Resolver) generateSBOM(root string) (report *trivy.Report, err error) {
seclog.Infof("Generating SBOM for %s", root)
r.sbomGenerations.Inc()

scanRequest := host.NewScanRequest(root, os.DirFS("/"))
scanRequest := host.NewScanRequest(root)
ch := collectors.GetHostScanner().Channel()
if ch == nil {
return nil, fmt.Errorf("couldn't retrieve global host scanner result channel")
Expand Down
9 changes: 9 additions & 0 deletions pkg/util/trivy/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

//go:build trivy

// Package trivy holds the scan components
package trivy
148 changes: 148 additions & 0 deletions pkg/util/trivy/containerd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ import (
"strings"
"time"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images/archive"
"github.com/containerd/containerd/leases"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/errdefs"
refdocker "github.com/distribution/reference"
api "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
Expand All @@ -31,6 +35,9 @@ import (
"github.com/samber/lo"

workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def"
"github.com/DataDog/datadog-agent/pkg/sbom"
cutil "github.com/DataDog/datadog-agent/pkg/util/containerd"
"github.com/DataDog/datadog-agent/pkg/util/log"
)

// ContainerdCollector defines the conttainerd collector name
Expand Down Expand Up @@ -181,3 +188,144 @@ func inspect(ctx context.Context, imgMeta *workloadmeta.ContainerImageMetadata,
},
}, history, ref, nil
}

const (
cleanupTimeout = 30 * time.Second
)

type fakeContainerdContainer struct {
*fakeContainer
*image
}

func (c *fakeContainerdContainer) LayerByDiffID(hash string) (ftypes.LayerPath, error) {
return c.fakeContainer.LayerByDiffID(hash)
}

func (c *fakeContainerdContainer) LayerByDigest(hash string) (ftypes.LayerPath, error) {
return c.fakeContainer.LayerByDigest(hash)
}

func (c *fakeContainerdContainer) Layers() (layers []ftypes.LayerPath) {
return c.fakeContainer.Layers()
}

// ContainerdAccessor is a function that should return a containerd client
type ContainerdAccessor func() (cutil.ContainerdItf, error)

// ScanContainerdImageFromSnapshotter scans containerd image directly from the snapshotter
func (c *Collector) ScanContainerdImageFromSnapshotter(ctx context.Context, imgMeta *workloadmeta.ContainerImageMetadata, img containerd.Image, client cutil.ContainerdItf, scanOptions sbom.ScanOptions) (sbom.Report, error) {
fanalImage, cleanup, err := convertContainerdImage(ctx, client.RawClient(), imgMeta, img)
if cleanup != nil {
defer cleanup()
}
if err != nil {
return nil, err
}

// Computing duration of containerd lease
deadline, _ := ctx.Deadline()
expiration := deadline.Sub(time.Now().Add(cleanupTimeout))
clClient := client.RawClient()
imageID := imgMeta.ID

mounts, err := client.Mounts(ctx, expiration, imgMeta.Namespace, img)
if err != nil {
return nil, fmt.Errorf("unable to get mounts for image %s, err: %w", imgMeta.ID, err)
}

layers := extractLayersFromOverlayFSMounts(mounts)
if len(layers) == 0 {
return nil, fmt.Errorf("unable to extract layers from overlayfs mounts %+v for image %s", mounts, imgMeta.ID)
}

ctx = namespaces.WithNamespace(ctx, imgMeta.Namespace)
// Adding a lease to cleanup dandling snaphots at expiration
ctx, done, err := clClient.WithLease(ctx,
leases.WithID(imageID),
leases.WithExpiration(expiration),
leases.WithLabels(map[string]string{
"containerd.io/gc.ref.snapshot." + containerd.DefaultSnapshotter: imageID,
}),
)
if err != nil && !errdefs.IsAlreadyExists(err) {
return nil, fmt.Errorf("unable to get a lease, err: %w", err)
}

report, err := c.scanOverlayFS(ctx, layers, &fakeContainerdContainer{
image: fanalImage,
fakeContainer: &fakeContainer{
layerPaths: layers,
imgMeta: imgMeta,
layerIDs: fanalImage.inspect.RootFS.Layers,
},
}, imgMeta, scanOptions)

if err := done(ctx); err != nil {
log.Warnf("Unable to cancel containerd lease with id: %s, err: %v", imageID, err)
}

return report, err
}

// ScanContainerdImage scans containerd image by exporting it and scanning the tarball
func (c *Collector) ScanContainerdImage(ctx context.Context, imgMeta *workloadmeta.ContainerImageMetadata, img containerd.Image, client cutil.ContainerdItf, scanOptions sbom.ScanOptions) (sbom.Report, error) {
fanalImage, cleanup, err := convertContainerdImage(ctx, client.RawClient(), imgMeta, img)
if cleanup != nil {
defer cleanup()
}
if err != nil {
return nil, fmt.Errorf("unable to convert containerd image, err: %w", err)
}

return c.scanImage(ctx, fanalImage, imgMeta, scanOptions)
}

// ScanContainerdImageFromFilesystem scans containerd image from file-system
func (c *Collector) ScanContainerdImageFromFilesystem(ctx context.Context, imgMeta *workloadmeta.ContainerImageMetadata, img containerd.Image, client cutil.ContainerdItf, scanOptions sbom.ScanOptions) (sbom.Report, error) {
imagePath, err := os.MkdirTemp("", "containerd-image-*")
if err != nil {
return nil, fmt.Errorf("unable to create temp dir, err: %w", err)
}
defer func() {
err := os.RemoveAll(imagePath)
if err != nil {
log.Errorf("Unable to remove temp dir: %s, err: %v", imagePath, err)
}
}()

// Computing duration of containerd lease
deadline, _ := ctx.Deadline()
expiration := deadline.Sub(time.Now().Add(cleanupTimeout))

cleanUp, err := client.MountImage(ctx, expiration, imgMeta.Namespace, img, imagePath)
if err != nil {
return nil, fmt.Errorf("unable to mount containerd image, err: %w", err)
}

defer func() {
cleanUpContext, cleanUpContextCancel := context.WithTimeout(context.Background(), cleanupTimeout)
err := cleanUp(cleanUpContext)
cleanUpContextCancel()
if err != nil {
log.Errorf("Unable to clean up mounted image, err: %v", err)
}
}()

return c.scanFilesystem(ctx, imagePath, imgMeta, scanOptions)
}

func extractLayersFromOverlayFSMounts(mounts []mount.Mount) []string {
var layers []string
for _, mount := range mounts {
for _, opt := range mount.Options {
for _, prefix := range []string{"upperdir=", "lowerdir="} {
trimmedOpt := strings.TrimPrefix(opt, prefix)
if trimmedOpt != opt {
layers = append(layers, strings.Split(trimmedOpt, ":")...)
}
}
}
}
return layers
}
94 changes: 94 additions & 0 deletions pkg/util/trivy/crio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

//go:build trivy

// Package trivy holds the scan components
package trivy

import (
"context"
"fmt"
"path/filepath"

workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def"
"github.com/DataDog/datadog-agent/pkg/sbom"
"github.com/DataDog/datadog-agent/pkg/util/crio"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
v1 "github.com/google/go-containerregistry/pkg/v1"
)

type fakeCRIOContainer struct {
*fakeContainer
}

func (c *fakeCRIOContainer) ID() (string, error) {
return c.imgMeta.ID, nil
}

func (c *fakeCRIOContainer) ConfigFile() (*v1.ConfigFile, error) {
configFile := &v1.ConfigFile{}
for _, layer := range c.imgMeta.Layers {
configFile.History = append(configFile.History, v1.History{
Author: layer.History.Author,
Created: v1.Time{Time: *layer.History.Created},
CreatedBy: layer.History.CreatedBy,
Comment: layer.History.Comment,
EmptyLayer: layer.History.EmptyLayer,
})

}
return configFile, nil
}

func (c *fakeCRIOContainer) LayerByDiffID(hash string) (ftypes.LayerPath, error) {
return c.fakeContainer.LayerByDiffID(hash)
}

func (c *fakeCRIOContainer) LayerByDigest(hash string) (ftypes.LayerPath, error) {
return c.fakeContainer.LayerByDigest(hash)
}

func (c *fakeCRIOContainer) Layers() (layers []ftypes.LayerPath) {
return c.fakeContainer.Layers()
}

func (c *fakeCRIOContainer) Name() string {
return c.imgMeta.Name
}

func (c *fakeCRIOContainer) RepoTags() []string {
return c.imgMeta.RepoTags
}

func (c *fakeCRIOContainer) RepoDigests() []string {
return c.imgMeta.RepoDigests
}

// ScanCRIOImageFromOverlayFS scans the CRI-O image layers using OverlayFS.
func (c *Collector) ScanCRIOImageFromOverlayFS(ctx context.Context, imgMeta *workloadmeta.ContainerImageMetadata, client crio.Client, scanOptions sbom.ScanOptions) (sbom.Report, error) {
lowerDirs, err := client.GetCRIOImageLayers(imgMeta)
if err != nil {
return nil, fmt.Errorf("failed to retrieve layer directories: %w", err)
}

var diffIDs []string
for _, dir := range lowerDirs {
diffIDs = append(diffIDs, filepath.Base(filepath.Dir(dir)))
}

report, err := c.scanOverlayFS(ctx, lowerDirs, &fakeCRIOContainer{
fakeContainer: &fakeContainer{
imgMeta: imgMeta,
layerPaths: lowerDirs,
layerIDs: diffIDs,
},
}, imgMeta, scanOptions)
if err != nil {
return nil, err
}

return report, nil
}
Loading

0 comments on commit 94636e8

Please sign in to comment.