github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/containers/containerd/containerd_test.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  package containerd_test
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"runtime"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/go-cmp/cmp/cmpopts"
    26  	"github.com/google/osv-scalibr/extractor"
    27  	"github.com/google/osv-scalibr/extractor/filesystem"
    28  	"github.com/google/osv-scalibr/extractor/filesystem/containers/containerd"
    29  	"github.com/google/osv-scalibr/extractor/filesystem/internal/units"
    30  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    31  	"github.com/google/osv-scalibr/inventory"
    32  )
    33  
    34  func TestFileRequired(t *testing.T) {
    35  	var e filesystem.Extractor = containerd.Extractor{}
    36  
    37  	tests := []struct {
    38  		name           string
    39  		path           string
    40  		onGoos         string
    41  		wantIsRequired bool
    42  	}{
    43  		{
    44  			name:           "containerd metadb linux",
    45  			path:           "var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db",
    46  			onGoos:         "linux",
    47  			wantIsRequired: true,
    48  		},
    49  		{
    50  			name: "containerd_metadb_windows",
    51  			path: "ProgramData/containerd/root/io.containerd.metadata.v1.bolt/meta.db",
    52  			// TODO(b/350963790): Enable this test case once the extractor is supported on Windows.
    53  			onGoos:         "ignore",
    54  			wantIsRequired: true,
    55  		},
    56  		{
    57  			name:           "random metadb linux",
    58  			path:           "var/lib/containerd/random/meta.db",
    59  			onGoos:         "linux",
    60  			wantIsRequired: false,
    61  		},
    62  		{
    63  			name:           "container metadb freebsd",
    64  			path:           "var/lib/containerd/random/meta.db",
    65  			onGoos:         "freebsd",
    66  			wantIsRequired: false,
    67  		},
    68  	}
    69  	for _, tt := range tests {
    70  		t.Run(tt.name, func(t *testing.T) {
    71  			if tt.onGoos != "" && tt.onGoos != runtime.GOOS {
    72  				t.Skipf("Skipping test on %s", runtime.GOOS)
    73  			}
    74  
    75  			isRequired := e.FileRequired(simplefileapi.New(tt.path, nil))
    76  			if isRequired != tt.wantIsRequired {
    77  				t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantIsRequired)
    78  			}
    79  		})
    80  	}
    81  }
    82  
    83  func TestExtract(t *testing.T) {
    84  	tests := []struct {
    85  		name              string
    86  		path              string
    87  		snapshotterdbpath string // path to metadata.db file, will be used for Linux test cases.
    88  		statusFilePath    string // path to status file, will be used for Linux test cases.
    89  		shimPIDFilePath   string // path to shim.pid, will be used for Windows test cases.
    90  		namespace         string
    91  		containerdID      string
    92  		cfg               containerd.Config
    93  		onGoos            string
    94  		wantPackages      []*extractor.Package
    95  		wantErr           error
    96  	}{
    97  		{
    98  			name:              "metadb valid linux",
    99  			path:              "testdata/meta_linux_test_single.db",
   100  			snapshotterdbpath: "testdata/metadata_linux_test.db",
   101  			statusFilePath:    "testdata/status",
   102  			namespace:         "k8s.io",
   103  			containerdID:      "b47fb93b51d091e16ae145b8b1438e5c011fd68cd65305fcd42fd83a13da7a8c",
   104  			cfg: containerd.Config{
   105  				MaxMetaDBFileSize: 500 * units.MiB,
   106  			},
   107  			onGoos: "linux",
   108  			wantPackages: []*extractor.Package{
   109  				{
   110  					Name:    "602401143452.dkr.ecr.us-west-2.amazonaws.com/eks/eks-pod-identity-agent:0.1.15",
   111  					Version: "sha256:832ad48c9872fdcae32f2ea369d9874fa34f2ea369d9874fa34f271b4dbc58ce04393c757befa462",
   112  					Metadata: &containerd.Metadata{
   113  						Namespace:   "k8s.io",
   114  						ImageName:   "602401143452.dkr.ecr.us-west-2.amazonaws.com/eks/eks-pod-identity-agent:0.1.15",
   115  						ImageDigest: "sha256:832ad48c9872fdcae32f2ea369d9874fa34f2ea369d9874fa34f271b4dbc58ce04393c757befa462",
   116  						Runtime:     "io.containerd.runc.v2",
   117  						ID:          "b47fb93b51d091e16ae145b8b1438e5c011fd68cd65305fcd42fd83a13da7a8c",
   118  						PID:         3530,
   119  						Snapshotter: "overlayfs",
   120  						SnapshotKey: "b47fb93b51d091e16ae145b8b1438e5c011fd68cd65305fcd42fd83a13da7a8c",
   121  						LowerDir:    "/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/14/fs:/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/13/fs:/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/7/fs",
   122  						UpperDir:    "/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/16/fs",
   123  						WorkDir:     "/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/16/work",
   124  					},
   125  					Locations: []string{"var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db"},
   126  				},
   127  			},
   128  		},
   129  		{
   130  			name:              "long lived metadata linux",
   131  			path:              "testdata/meta_linux_test_long_lived.db",
   132  			snapshotterdbpath: "testdata/metadata_linux_test_long_lived.db",
   133  			statusFilePath:    "testdata/status_long_lived",
   134  			namespace:         "k8s.io",
   135  			containerdID:      "b0653b5a8357310c1f18d680cb26c559a8cc9595002888cf542affaaeeb30e99",
   136  			cfg: containerd.Config{
   137  				MaxMetaDBFileSize: 500 * units.MiB,
   138  			},
   139  			onGoos: "linux",
   140  			wantPackages: []*extractor.Package{
   141  				{
   142  					Name:    "us-docker.pkg.dev/google-samples/containers/gke/security/maven-vulns:latest",
   143  					Version: "sha256:2de1666a491de0d56f4b204a51fedbc27b21a6211c67bfacbce56f18a7fb06ee",
   144  					Metadata: &containerd.Metadata{
   145  						Namespace:    "k8s.io",
   146  						ImageName:    "us-docker.pkg.dev/google-samples/containers/gke/security/maven-vulns:latest",
   147  						ImageDigest:  "sha256:2de1666a491de0d56f4b204a51fedbc27b21a6211c67bfacbce56f18a7fb06ee",
   148  						Runtime:      "io.containerd.runc.v2",
   149  						ID:           "b0653b5a8357310c1f18d680cb26c559a8cc9595002888cf542affaaeeb30e99",
   150  						PID:          2357250,
   151  						PodName:      "maven-vulns-58444c9f5d-scl4g",
   152  						PodNamespace: "default",
   153  						Snapshotter:  "overlayfs",
   154  						SnapshotKey:  "b0653b5a8357310c1f18d680cb26c559a8cc9595002888cf542affaaeeb30e99",
   155  						LowerDir:     "/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/442/fs:/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/441/fs:/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/440/fs:/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/439/fs",
   156  						UpperDir:     "/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/443/fs",
   157  						WorkDir:      "/tmp/TestExtractmetadb_valid_linux1567346986/001/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/443/work",
   158  					},
   159  					Locations: []string{"var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db"},
   160  				},
   161  			},
   162  		},
   163  		{
   164  			name:              "metadb invalid",
   165  			path:              "testdata/invalid_meta.db",
   166  			statusFilePath:    "testdata/status",
   167  			snapshotterdbpath: "testdata/metadata_linux_test.db",
   168  			namespace:         "default",
   169  			containerdID:      "test_pod",
   170  			onGoos:            "linux",
   171  			cfg: containerd.Config{
   172  				MaxMetaDBFileSize: 500 * units.MiB,
   173  			},
   174  			wantPackages: nil,
   175  			wantErr:      cmpopts.AnyError,
   176  		},
   177  		{
   178  			name:              "metadb too large",
   179  			path:              "testdata/meta_linux_too_big.db",
   180  			statusFilePath:    "testdata/status",
   181  			snapshotterdbpath: "testdata/metadata_linux_test.db",
   182  			namespace:         "default",
   183  			containerdID:      "test_pod",
   184  			onGoos:            "linux",
   185  			cfg: containerd.Config{
   186  				MaxMetaDBFileSize: 1 * units.KiB,
   187  			},
   188  			wantPackages: nil,
   189  			wantErr:      cmpopts.AnyError,
   190  		},
   191  		{
   192  			name:              "invalid status file",
   193  			path:              "testdata/meta_linux_test_single.db",
   194  			statusFilePath:    "testdata/invalid_status",
   195  			snapshotterdbpath: "testdata/metadata_linux_test.db",
   196  			namespace:         "k8s.io",
   197  			containerdID:      "b47fb93b51d091e16ae145b8b1438e5c011fd68cd65305fcd42fd83a13da7a8c",
   198  			onGoos:            "linux",
   199  			cfg: containerd.Config{
   200  				MaxMetaDBFileSize: 500 * units.MiB,
   201  			},
   202  			wantPackages: []*extractor.Package{},
   203  		},
   204  		{
   205  			name:            "metadb valid windows",
   206  			path:            "testdata/meta_windows.db",
   207  			shimPIDFilePath: "testdata/shim.pid",
   208  			namespace:       "default",
   209  			containerdID:    "test_pod",
   210  			cfg: containerd.Config{
   211  				MaxMetaDBFileSize: 500 * units.MiB,
   212  			},
   213  			// TODO(b/350963790): Enable this test case once the extractor is supported on Windows.
   214  			onGoos: "ignore",
   215  			wantPackages: []*extractor.Package{
   216  				{
   217  					Name:    "mcr.microsoft.com/windows/nanoserver:ltsc2022",
   218  					Version: "sha256:31c8aa02d47af7d65c11da9c3a279c8407c32afd3fc6bec2e9a544db8e3715b3",
   219  					Metadata: &containerd.Metadata{
   220  						Namespace:   "default",
   221  						ImageName:   "mcr.microsoft.com/windows/nanoserver:ltsc2022",
   222  						ImageDigest: "sha256:31c8aa02d47af7d65c11da9c3a279c8407c32afd3fc6bec2e9a544db8e3715b3",
   223  						Runtime:     "io.containerd.runhcs.v1",
   224  						ID:          "test_pod",
   225  						PID:         5628,
   226  					},
   227  					Locations: []string{"ProgramData/containerd/root/io.containerd.metadata.v1.bolt/meta.db"},
   228  				},
   229  			},
   230  		},
   231  		{
   232  			name:            "invalid shim pid",
   233  			path:            "testdata/meta_windows.db",
   234  			shimPIDFilePath: "testdata/state.json",
   235  			namespace:       "default",
   236  			containerdID:    "test_pod",
   237  			// TODO(b/350963790): Enable this test case once the extractor is supported on Windows.
   238  			onGoos: "ignore",
   239  			cfg: containerd.Config{
   240  				MaxMetaDBFileSize: 500 * units.MiB,
   241  			},
   242  			wantPackages: []*extractor.Package{},
   243  		},
   244  	}
   245  
   246  	for _, tt := range tests {
   247  		t.Run(tt.name, func(t *testing.T) {
   248  			if tt.onGoos != "" && tt.onGoos != runtime.GOOS {
   249  				t.Skipf("Skipping test on %s", runtime.GOOS)
   250  			}
   251  
   252  			var input *filesystem.ScanInput
   253  			d := "/tmp/TestExtractmetadb_valid_linux1567346986/001"
   254  			if tt.onGoos == "linux" {
   255  				containerStatusPath := filepath.Join("var/lib/containerd/io.containerd.grpc.v1.cri/containers/", tt.containerdID)
   256  				createFileFromTestData(t, d, "var/lib/containerd/io.containerd.metadata.v1.bolt", "meta.db", tt.path)
   257  				createFileFromTestData(t, d, "var/lib/containerd/io.containerd.snapshotter.v1.overlayfs", "metadata.db", tt.snapshotterdbpath)
   258  				createFileFromTestData(t, d, containerStatusPath, "status", tt.statusFilePath)
   259  				input = createScanInput(t, d, "var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db")
   260  			}
   261  			if tt.onGoos == "windows" {
   262  				createFileFromTestData(t, d, "ProgramData/containerd/root/io.containerd.metadata.v1.bolt", "meta.db", tt.path)
   263  				createFileFromTestData(t, d, filepath.Join("ProgramData/containerd/state/io.containerd.runtime.v2.task/", tt.namespace, tt.containerdID), "shim.pid", tt.shimPIDFilePath)
   264  				input = createScanInput(t, d, "ProgramData/containerd/root/io.containerd.metadata.v1.bolt/meta.db")
   265  			}
   266  
   267  			e := containerd.New(defaultConfigWith(tt.cfg))
   268  			got, err := e.Extract(t.Context(), input)
   269  			if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) {
   270  				t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.path, err, tt.wantErr)
   271  			}
   272  
   273  			ignoreOrder := cmpopts.SortSlices(func(a, b any) bool {
   274  				return fmt.Sprintf("%+v", a) < fmt.Sprintf("%+v", b)
   275  			})
   276  			wantInv := inventory.Inventory{Packages: tt.wantPackages}
   277  			if diff := cmp.Diff(wantInv, got, ignoreOrder); diff != "" {
   278  				t.Errorf("Extract(%s) (-want +got):\n%s", tt.path, diff)
   279  			}
   280  			// Remove all files and the test directory.
   281  			err = os.RemoveAll(d)
   282  			if err != nil {
   283  				t.Fatalf("Failed to remove test directory after the test: %v", err)
   284  			}
   285  		})
   286  	}
   287  }
   288  
   289  //nolint:unparam
   290  func createFileFromTestData(t *testing.T, root string, subPath string, fileName string, testDataFilePath string) {
   291  	t.Helper()
   292  	_ = os.MkdirAll(filepath.Join(root, subPath), 0755)
   293  	testData, err := os.ReadFile(testDataFilePath)
   294  	if err != nil {
   295  		t.Fatalf("read from %s: %v\n", testDataFilePath, err)
   296  	}
   297  	err = os.WriteFile(filepath.Join(root, subPath, fileName), testData, 0644)
   298  	if err != nil {
   299  		t.Fatalf("write to %s: %v\n", filepath.Join(root, subPath, fileName), err)
   300  	}
   301  }
   302  
   303  func createScanInput(t *testing.T, root string, path string) *filesystem.ScanInput {
   304  	t.Helper()
   305  
   306  	finalPath := filepath.Join(root, path)
   307  	reader, err := os.Open(finalPath)
   308  	defer func() {
   309  		if err = reader.Close(); err != nil {
   310  			t.Errorf("Close(): %v", err)
   311  		}
   312  	}()
   313  	if err != nil {
   314  		t.Fatal(err)
   315  	}
   316  
   317  	info, err := os.Stat(finalPath)
   318  	if err != nil {
   319  		t.Fatal(err)
   320  	}
   321  	input := &filesystem.ScanInput{Path: path, Reader: reader, Root: root, Info: info}
   322  	return input
   323  }
   324  
   325  // defaultConfigWith combines any non-zero fields of cfg with packagejson.DefaultConfig().
   326  func defaultConfigWith(cfg containerd.Config) containerd.Config {
   327  	newCfg := containerd.DefaultConfig()
   328  
   329  	if cfg.MaxMetaDBFileSize > 0 {
   330  		newCfg.MaxMetaDBFileSize = cfg.MaxMetaDBFileSize
   331  	}
   332  
   333  	return newCfg
   334  }