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 }