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 }