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  }