github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/embeddedfs/ova/ova_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 ova_test 16 17 import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "io" 22 "os" 23 "path/filepath" 24 "strings" 25 "testing" 26 27 "archive/tar" 28 29 cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto" 30 "github.com/google/osv-scalibr/extractor/filesystem" 31 "github.com/google/osv-scalibr/extractor/filesystem/embeddedfs/ova" 32 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 33 "github.com/google/osv-scalibr/testing/fakefs" 34 ) 35 36 func TestFileRequired(t *testing.T) { 37 tests := []struct { 38 desc string 39 path string 40 fileSize int64 41 maxFileSize int64 42 pluginSpecificMaxSize int64 43 want bool 44 }{ 45 { 46 desc: "ova_lowercase", 47 path: "testdata/disk.ova", 48 want: true, 49 }, 50 { 51 desc: "ova_uppercase", 52 path: "testdata/DISK.OVA", 53 want: true, 54 }, 55 { 56 desc: "not_ova", 57 path: "testdata/document.txt", 58 want: false, 59 }, 60 { 61 desc: "no_extension", 62 path: "testdata/noextension", 63 want: false, 64 }, 65 { 66 desc: "file_size_below_limit", 67 path: "disk.ova", 68 fileSize: 1000, 69 maxFileSize: 1000, 70 want: true, 71 }, 72 { 73 desc: "file_size_above_limit", 74 path: "disk.ova", 75 fileSize: 1001, 76 maxFileSize: 1000, 77 want: false, 78 }, 79 { 80 desc: "override_global_size_below_limit", 81 path: "disk.ova", 82 fileSize: 1001, 83 maxFileSize: 1000, 84 pluginSpecificMaxSize: 1001, 85 want: true, 86 }, 87 { 88 desc: "override_global_size_above_limit", 89 path: "disk.ova", 90 fileSize: 1001, 91 maxFileSize: 1001, 92 pluginSpecificMaxSize: 1000, 93 want: false, 94 }, 95 } 96 97 for _, tt := range tests { 98 extractor := ova.New(&cpb.PluginConfig{ 99 MaxFileSizeBytes: tt.maxFileSize, 100 PluginSpecific: []*cpb.PluginSpecificConfig{ 101 {Config: &cpb.PluginSpecificConfig_Ova{Ova: &cpb.OVAConfig{MaxFileSizeBytes: tt.pluginSpecificMaxSize}}}, 102 }, 103 }) 104 t.Run(tt.desc, func(t *testing.T) { 105 if got := extractor.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 106 FileSize: tt.fileSize, 107 })); got != tt.want { 108 t.Errorf("FileRequired(%q) = %v, want %v", tt.path, got, tt.want) 109 } 110 }) 111 } 112 } 113 114 func TestExtractValidOVA(t *testing.T) { 115 extractor := ova.New(&cpb.PluginConfig{}) 116 path := filepath.FromSlash("testdata/valid.ova") 117 info, err := os.Stat(path) 118 if err != nil { 119 t.Fatalf("os.Stat(%q) failed: %v", path, err) 120 } 121 122 f, err := os.Open(path) 123 if err != nil { 124 t.Fatalf("os.Open(%q) failed: %v", path, err) 125 } 126 defer f.Close() 127 128 input := &filesystem.ScanInput{ 129 Path: path, 130 Root: ".", 131 Info: info, 132 Reader: f, 133 FS: nil, 134 } 135 136 ctx := t.Context() 137 inv, err := extractor.Extract(ctx, input) 138 if err != nil { 139 t.Fatalf("Extract(%q) failed: %v", path, err) 140 } 141 142 if len(inv.EmbeddedFSs) == 0 { 143 t.Fatal("Extract returned nothing") 144 } 145 146 for i, embeddedFS := range inv.EmbeddedFSs { 147 t.Run(fmt.Sprintf("OVAImage_%d", i), func(t *testing.T) { 148 if !strings.HasPrefix(embeddedFS.Path, path) { 149 t.Errorf("EmbeddedFS.Path = %q, want prefix %q", embeddedFS.Path, path) 150 } 151 152 fs, err := embeddedFS.GetEmbeddedFS(ctx) 153 if err != nil { 154 t.Errorf("GetEmbeddedFS() failed: %v", err) 155 } 156 157 entries, err := fs.ReadDir("/") 158 if err != nil { 159 t.Fatalf("fs.ReadDir(/) failed: %v", err) 160 } 161 t.Logf("ReadDir(/) returned %d entries", len(entries)) 162 163 info, err := fs.Stat("/") 164 if err != nil { 165 t.Fatalf("fs.Stat(/) failed: %v", err) 166 } 167 if !info.IsDir() { 168 t.Errorf("fs.Stat(/) IsDir() = %v, want true", info.IsDir()) 169 } 170 171 found := false 172 for _, entry := range entries { 173 name := entry.Name() 174 if strings.HasSuffix(name, ".ovf") { 175 found = true 176 filePath := name 177 f, err := fs.Open(filePath) 178 if err != nil { 179 t.Fatalf("fs.Open(%q) failed: %v", filePath, err) 180 } 181 defer f.Close() 182 183 buf := make([]byte, 5) 184 n, err := f.Read(buf) 185 if err != nil && !errors.Is(err, io.EOF) { 186 t.Errorf("f.Read(%q) failed: %v", filePath, err) 187 } 188 t.Logf("Read %d bytes from %s\n", n, name) 189 190 // The buffer must start with "<?xml" 191 if string(buf[:5]) != "<?xml" { 192 t.Errorf("%s contains unexpected data!", filePath) 193 } 194 195 info, err := f.Stat() 196 if err != nil { 197 t.Errorf("f.Stat(%q) failed: %v", filePath, err) 198 } else if info.IsDir() { 199 t.Errorf("f.Stat(%q) IsDir() = %v, want false", filePath, info.IsDir()) 200 } 201 break 202 } 203 } 204 if !found { 205 t.Errorf("ovf file not found") 206 } 207 }) 208 } 209 } 210 211 func TestExtractMaliciousOVA(t *testing.T) { 212 // Create a malicious tar archive in memory 213 var buf bytes.Buffer 214 tw := tar.NewWriter(&buf) 215 216 // Create a header with "../../../../../../../../../file.txt" 217 // to simulate a path traversal entry 218 hdr := &tar.Header{ 219 Name: "../../../../../../../../../file.txt", 220 Mode: 0600, 221 Size: int64(len("malicious content")), 222 Typeflag: tar.TypeReg, 223 } 224 if err := tw.WriteHeader(hdr); err != nil { 225 t.Fatalf("WriteHeader failed: %v", err) 226 } 227 if _, err := tw.Write([]byte("malicious content")); err != nil { 228 t.Fatalf("Write failed: %v", err) 229 } 230 tw.Close() 231 232 extractor := ova.New(&cpb.PluginConfig{}) 233 input := &filesystem.ScanInput{ 234 Path: "", 235 Root: "testdata", 236 Info: nil, 237 Reader: bytes.NewReader(buf.Bytes()), // provide the in-memory tar data 238 FS: nil, 239 } 240 241 ctx := t.Context() 242 var err error 243 _, err = extractor.Extract(ctx, input) 244 if err == nil { 245 t.Errorf("Extract succeeded, want error for parent path entry") 246 } else if !strings.Contains(err.Error(), "invalid entries") { 247 t.Errorf("Extract error = %v, want 'invalid entries'", err) 248 } 249 } 250 251 func TestExtractInvalidOVA(t *testing.T) { 252 extractor := ova.New(&cpb.PluginConfig{}) 253 path := filepath.FromSlash("testdata/invalid.ova") 254 info, err := os.Stat(path) 255 if err != nil { 256 t.Fatalf("os.Stat(%q) failed: %v", path, err) 257 } 258 259 f, err := os.Open(path) 260 if err != nil { 261 t.Fatalf("os.Open(%q) failed: %v", path, err) 262 } 263 defer f.Close() 264 265 input := &filesystem.ScanInput{ 266 Path: path, 267 Root: ".", 268 Info: info, 269 Reader: f, 270 FS: nil, 271 } 272 273 ctx := t.Context() 274 _, err = extractor.Extract(ctx, input) 275 if err == nil { 276 t.Errorf("Extract(%q) succeeded, want error", path) 277 } 278 } 279 280 func TestExtractNonExistentOVA(t *testing.T) { 281 extractor := ova.New(&cpb.PluginConfig{}) 282 path := filepath.FromSlash("testdata/nonexistent.ova") 283 input := &filesystem.ScanInput{ 284 Path: path, 285 Root: "testdata", 286 Info: nil, 287 Reader: nil, 288 FS: nil, 289 } 290 291 ctx := t.Context() 292 _, err := extractor.Extract(ctx, input) 293 if err == nil { 294 t.Errorf("Extract(%q) succeeded, want error", path) 295 } 296 }