github.com/google/osv-scalibr@v0.4.1/extractor/standalone/containers/containerd/containerd_linux.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  //go:build linux
    16  
    17  // Package containerd extracts container inventory from containerd API.
    18  package containerd
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	containerd "github.com/containerd/containerd"
    27  	tasks "github.com/containerd/containerd/api/services/tasks/v1"
    28  	task "github.com/containerd/containerd/api/types/task"
    29  	"github.com/containerd/containerd/namespaces"
    30  	"github.com/google/osv-scalibr/extractor"
    31  	"github.com/google/osv-scalibr/extractor/standalone"
    32  	"github.com/google/osv-scalibr/extractor/standalone/containers/containerd/containerdmetadata"
    33  	"github.com/google/osv-scalibr/inventory"
    34  	"github.com/google/osv-scalibr/log"
    35  	"github.com/google/osv-scalibr/plugin"
    36  )
    37  
    38  const (
    39  	// Name is the unique name of this extractor.
    40  	Name = "containers/containerd-runtime"
    41  
    42  	// defaultContainerdSocketAddr is the default path to the containerd socket.
    43  	defaultContainerdSocketAddr = "/run/containerd/containerd.sock"
    44  
    45  	// defaultContainerdRootfsPrefix is the default path to the containerd tasks rootfs.
    46  	// It is used if containerd API does not return the rootfs path in the container spec.
    47  	defaultContainerdRootfsPrefix = "/run/containerd/io.containerd.runtime.v2.task/"
    48  )
    49  
    50  // CtrdClient is an interface that provides an abstraction on top of the containerd client.
    51  // Needed for testing purposes.
    52  type CtrdClient interface {
    53  	LoadContainer(ctx context.Context, id string) (containerd.Container, error)
    54  	NamespaceService() namespaces.Store
    55  	TaskService() tasks.TasksClient
    56  	Close() error
    57  }
    58  
    59  // Config is the configuration for the Extractor.
    60  type Config struct {
    61  	// ContainerdSocketAddr is the local path to the containerd socket.
    62  	// Used further to crete a client for containerd API.
    63  	ContainerdSocketAddr string
    64  }
    65  
    66  // DefaultConfig returns the default configuration for the containerd extractor.
    67  func DefaultConfig() Config {
    68  	return Config{
    69  		ContainerdSocketAddr: defaultContainerdSocketAddr,
    70  	}
    71  }
    72  
    73  // Extractor implements the containerd runtime extractor.
    74  type Extractor struct {
    75  	client              CtrdClient
    76  	socketAddr          string
    77  	checkIfSocketExists bool
    78  	initNewCtrdClient   bool
    79  }
    80  
    81  // New creates a new containerd client and returns a containerd container inventory extractor.
    82  func New(cfg Config) standalone.Extractor {
    83  	return &Extractor{
    84  		client:              nil,
    85  		socketAddr:          cfg.ContainerdSocketAddr,
    86  		checkIfSocketExists: true,
    87  		initNewCtrdClient:   true,
    88  	}
    89  }
    90  
    91  // NewDefault returns an extractor with the default config settings.
    92  func NewDefault() standalone.Extractor {
    93  	return New(DefaultConfig())
    94  }
    95  
    96  // NewWithClient creates a new extractor with the provided containerd client.
    97  // Needed for testing purposes.
    98  func NewWithClient(cli CtrdClient, socketAddr string) *Extractor {
    99  	// Uses the provided containerd client and just returns the extractor.
   100  	return &Extractor{
   101  		client:              cli,
   102  		socketAddr:          socketAddr,
   103  		checkIfSocketExists: false, // Not needed if client already provided.
   104  		initNewCtrdClient:   false, // Not needed if client already provided.
   105  	}
   106  }
   107  
   108  // Config returns the configuration of the extractor.
   109  func (e Extractor) Config() Config {
   110  	return Config{
   111  		ContainerdSocketAddr: e.socketAddr,
   112  	}
   113  }
   114  
   115  // Name of the extractor.
   116  func (e Extractor) Name() string { return Name }
   117  
   118  // Version of the extractor.
   119  func (e Extractor) Version() int { return 0 }
   120  
   121  // Requirements of the extractor.
   122  func (e Extractor) Requirements() *plugin.Capabilities {
   123  	return &plugin.Capabilities{
   124  		OS:            plugin.OSLinux,
   125  		RunningSystem: true,
   126  	}
   127  }
   128  
   129  // Extract extracts containers from the containerd API.
   130  func (e *Extractor) Extract(ctx context.Context, input *standalone.ScanInput) (inventory.Inventory, error) {
   131  	var result = []*extractor.Package{}
   132  	if e.checkIfSocketExists {
   133  		if _, err := os.Stat(e.socketAddr); err != nil {
   134  			log.Infof("Containerd socket %v does not exist, skipping extraction.", e.socketAddr)
   135  			return inventory.Inventory{}, err
   136  		}
   137  	}
   138  	// Creating client here instead of New() to prevent client creation when extractor is not in use.
   139  	if e.initNewCtrdClient {
   140  		// Create a new containerd API client using the provided socket address
   141  		// and reset it in the extractor.
   142  		cli, err := containerd.New(e.socketAddr)
   143  		if err != nil {
   144  			log.Errorf("Failed to connect to containerd socket %v, error: %v", e.socketAddr, err)
   145  			return inventory.Inventory{}, err
   146  		}
   147  		e.client = cli
   148  		e.initNewCtrdClient = false
   149  	}
   150  
   151  	if e.client == nil {
   152  		return inventory.Inventory{}, errors.New("containerd API client is not initialized")
   153  	}
   154  
   155  	ctrMetadata, err := containersFromAPI(ctx, e.client)
   156  	if err != nil {
   157  		log.Errorf("Could not get container package from the containerd: %v", err)
   158  		return inventory.Inventory{}, err
   159  	}
   160  
   161  	for _, ctr := range ctrMetadata {
   162  		pkg := &extractor.Package{
   163  			Name:      ctr.ImageName,
   164  			Version:   ctr.ImageDigest,
   165  			Locations: []string{ctr.RootFS},
   166  			Metadata:  &ctr,
   167  		}
   168  		result = append(result, pkg)
   169  	}
   170  
   171  	defer e.client.Close()
   172  	return inventory.Inventory{Packages: result}, nil
   173  }
   174  
   175  func containersFromAPI(ctx context.Context, client CtrdClient) ([]containerdmetadata.Metadata, error) {
   176  	var metadata []containerdmetadata.Metadata
   177  
   178  	// Get list of namespaces from the containerd API.
   179  	nss, err := namespacesFromAPI(ctx, client)
   180  	if err != nil {
   181  		log.Errorf("Could not get a list of namespaces from the containerd: %v", err)
   182  		return nil, err
   183  	}
   184  
   185  	for _, ns := range nss {
   186  		// For each namespace returned by the API, get the containers metadata.
   187  		ctx := namespaces.WithNamespace(ctx, ns)
   188  		ctrs := containersMetadata(ctx, client, ns, defaultContainerdRootfsPrefix)
   189  		// Merge all containers metadata items for all namespaces into a single list.
   190  		metadata = append(metadata, ctrs...)
   191  	}
   192  	return metadata, nil
   193  }
   194  
   195  func namespacesFromAPI(ctx context.Context, client CtrdClient) ([]string, error) {
   196  	nsService := client.NamespaceService()
   197  	nss, err := nsService.List(ctx)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	return nss, nil
   203  }
   204  
   205  func containersMetadata(ctx context.Context, client CtrdClient, namespace string, defaultAbsoluteToBundlePath string) []containerdmetadata.Metadata {
   206  	var containersMetadata []containerdmetadata.Metadata
   207  
   208  	taskService := client.TaskService()
   209  	// List all running tasks, only running tasks have a container associated with them.
   210  	listTasksReq := &tasks.ListTasksRequest{Filter: "status=running"}
   211  	listTasksResp, err := taskService.List(ctx, listTasksReq)
   212  	if err != nil {
   213  		log.Errorf("Failed to list tasks: %v", err)
   214  	}
   215  
   216  	// For each running task, get the container information associated with it.
   217  	for _, task := range listTasksResp.Tasks {
   218  		md, err := taskMetadata(ctx, client, task, namespace, defaultAbsoluteToBundlePath)
   219  		if err != nil {
   220  			log.Errorf("Failed to get task metadata for task %v: %v", task.ID, err)
   221  			continue
   222  		}
   223  
   224  		containersMetadata = append(containersMetadata, md)
   225  	}
   226  	return containersMetadata
   227  }
   228  
   229  func taskMetadata(ctx context.Context, client CtrdClient, task *task.Process, namespace string, defaultAbsoluteToBundlePath string) (containerdmetadata.Metadata, error) {
   230  	var md containerdmetadata.Metadata
   231  
   232  	container, err := client.LoadContainer(ctx, task.ID)
   233  	if err != nil {
   234  		log.Errorf("Failed to load container for task %v, error: %v", task.ID, err)
   235  		return md, err
   236  	}
   237  
   238  	info, err := container.Info(ctx)
   239  	if err != nil {
   240  		log.Errorf("Failed to obtain container info for container %v, error: %v", task.ID, err)
   241  		return md, err
   242  	}
   243  
   244  	image, err := container.Image(ctx)
   245  	if err != nil {
   246  		log.Errorf("Failed to obtain container image for container %v, error: %v", task.ID, err)
   247  		return md, err
   248  	}
   249  
   250  	ctdTask, err := container.Task(ctx, nil)
   251  	if err != nil {
   252  		log.Errorf("Failed to obtain containerd container task data for container %v, error: %v", task.ID, err)
   253  		return md, err
   254  	}
   255  
   256  	spec, err := ctdTask.Spec(ctx)
   257  	if err != nil {
   258  		log.Errorf("Failed to obtain containerd container task spec for container %v, error: %v", task.ID, err)
   259  		return md, err
   260  	}
   261  	// Defined in https://github.com/opencontainers/runtime-spec/blob/main/config.md#root. For POSIX
   262  	// platforms, path is either an absolute path or a relative path to the bundle. examples as below:
   263  	// "/run/containerd/io.containerd.runtime.v2.task/default/nginx-test/rootfs" or "rootfs".
   264  	rootfs := ""
   265  	switch {
   266  	case filepath.IsAbs(spec.Root.Path):
   267  		rootfs = spec.Root.Path
   268  	case spec.Root.Path != "":
   269  		log.Infof("Rootfs is a relative path for a container: %v, concatenating rootfs path prefix", task.ID)
   270  		rootfs = filepath.Join(defaultAbsoluteToBundlePath, namespace, task.ID, spec.Root.Path)
   271  	case spec.Root.Path == "":
   272  		log.Infof("Rootfs is empty for a container: %v, using default rootfs path prefix", task.ID)
   273  		rootfs = filepath.Join(defaultAbsoluteToBundlePath, namespace, task.ID, "rootfs")
   274  	}
   275  
   276  	name := info.Image
   277  	runtime := info.Runtime.Name
   278  	digest := image.Target().Digest.String()
   279  	pid := int(task.Pid)
   280  
   281  	md = containerdmetadata.Metadata{
   282  		Namespace:   namespace,
   283  		ImageName:   name,
   284  		ImageDigest: digest,
   285  		Runtime:     runtime,
   286  		ID:          task.ID,
   287  		PID:         pid,
   288  		RootFS:      rootfs,
   289  	}
   290  
   291  	return md, nil
   292  }