github.com/google/osv-scalibr@v0.4.1/annotator/osduplicate/cos/cos_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 cos_test
    16  
    17  import (
    18  	"context"
    19  	"os"
    20  	"path/filepath"
    21  	"testing"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	"github.com/google/go-cmp/cmp/cmpopts"
    25  	"github.com/google/go-cpy/cpy"
    26  	"github.com/google/osv-scalibr/annotator"
    27  	"github.com/google/osv-scalibr/annotator/osduplicate/cos"
    28  	"github.com/google/osv-scalibr/extractor"
    29  	cosextractor "github.com/google/osv-scalibr/extractor/filesystem/os/cos"
    30  	scalibrfs "github.com/google/osv-scalibr/fs"
    31  	"github.com/google/osv-scalibr/inventory"
    32  	"github.com/google/osv-scalibr/inventory/vex"
    33  	"google.golang.org/protobuf/proto"
    34  )
    35  
    36  const (
    37  	cosPackageInfoFile = "etc/cos-package-info.json"
    38  )
    39  
    40  func TestAnnotate(t *testing.T) {
    41  	cancelledContext, cancel := context.WithCancel(t.Context())
    42  	cancel()
    43  
    44  	copier := cpy.New(
    45  		cpy.Func(proto.Clone),
    46  		cpy.IgnoreAllUnexported(),
    47  	)
    48  
    49  	tests := []struct {
    50  		desc string
    51  		// If nil, a default COS filesystem will be used.
    52  		input    *annotator.ScanInput
    53  		packages []*extractor.Package
    54  		//nolint:containedctx
    55  		ctx          context.Context
    56  		wantErr      error
    57  		wantPackages []*extractor.Package
    58  	}{
    59  		{
    60  			desc: "some_pkgs_found_in_cos_pkg_folder",
    61  			packages: []*extractor.Package{
    62  				{
    63  					Name:      "file-in-cos-pkgs",
    64  					Locations: []string{"mnt/stateful_partition/var_overlay/db/pkg/path/to/file-in-cos-pkgs"},
    65  				},
    66  				{
    67  					Name:      "file-not-in-cos-pkgs",
    68  					Locations: []string{"mnt/stateful_partition/file/not/in/pkgs"},
    69  				},
    70  			},
    71  			wantPackages: []*extractor.Package{
    72  				{
    73  					Name:      "file-in-cos-pkgs",
    74  					Locations: []string{"mnt/stateful_partition/var_overlay/db/pkg/path/to/file-in-cos-pkgs"},
    75  					ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{
    76  						Plugin:          cos.Name,
    77  						Justification:   vex.ComponentNotPresent,
    78  						MatchesAllVulns: true,
    79  					}},
    80  				},
    81  				{
    82  					Name:      "file-not-in-cos-pkgs",
    83  					Locations: []string{"mnt/stateful_partition/file/not/in/pkgs"},
    84  				},
    85  			},
    86  		},
    87  		{
    88  			desc: "some_pkgs_outside_mutable_dir",
    89  			packages: []*extractor.Package{
    90  				{
    91  					Name:      "file-in-mutable-dir",
    92  					Locations: []string{"mnt/stateful_partition/in/mutable/dir"},
    93  				},
    94  				{
    95  					Name:      "file-not-in-mutable-dir",
    96  					Locations: []string{"not/in/mutable/dir"},
    97  				},
    98  			},
    99  			wantPackages: []*extractor.Package{
   100  				{
   101  					Name:      "file-in-mutable-dir",
   102  					Locations: []string{"mnt/stateful_partition/in/mutable/dir"},
   103  				},
   104  				{
   105  					Name:      "file-not-in-mutable-dir",
   106  					Locations: []string{"not/in/mutable/dir"},
   107  					ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{
   108  						Plugin:          cos.Name,
   109  						Justification:   vex.ComponentNotPresent,
   110  						MatchesAllVulns: true,
   111  					}},
   112  				},
   113  			},
   114  		},
   115  		{
   116  			desc: "pkgs_found_in_non_cos_filesystem",
   117  			input: &annotator.ScanInput{
   118  				ScanRoot: scalibrfs.RealFSScanRoot(t.TempDir()),
   119  			},
   120  			packages: []*extractor.Package{
   121  				{
   122  					Name:      "file-in-cos-pkgs",
   123  					Locations: []string{"mnt/stateful_partition/var_overlay/db/pkg/path/to/file-in-cos-pkgs"},
   124  				},
   125  				{
   126  					Name:      "file-not-in-cos-pkgs",
   127  					Locations: []string{"mnt/stateful_partition/file/not/in/pkgs"},
   128  				},
   129  			},
   130  			wantPackages: []*extractor.Package{
   131  				{
   132  					Name:      "file-in-cos-pkgs",
   133  					Locations: []string{"mnt/stateful_partition/var_overlay/db/pkg/path/to/file-in-cos-pkgs"},
   134  					// Expect no exploitability signals.
   135  				},
   136  				{
   137  					Name:      "file-not-in-cos-pkgs",
   138  					Locations: []string{"mnt/stateful_partition/file/not/in/pkgs"},
   139  				},
   140  			},
   141  		},
   142  		{
   143  			desc: "cos_os_packages",
   144  			packages: []*extractor.Package{
   145  				{
   146  					Name:      "os-pkg",
   147  					Locations: []string{"etc/cos-package-info.json"},
   148  					Plugins:   []string{cosextractor.Name},
   149  				},
   150  			},
   151  			wantPackages: []*extractor.Package{
   152  				{
   153  					Name:      "os-pkg",
   154  					Locations: []string{"etc/cos-package-info.json"},
   155  					Plugins:   []string{cosextractor.Name},
   156  				},
   157  			},
   158  		},
   159  		{
   160  			desc:         "pkg_has_no_location",
   161  			packages:     []*extractor.Package{{Name: "file"}},
   162  			wantPackages: []*extractor.Package{{Name: "file"}},
   163  		},
   164  		{
   165  			desc: "ctx_cancelled",
   166  			ctx:  cancelledContext,
   167  			packages: []*extractor.Package{
   168  				{
   169  					Name:      "file-in-cos-pkgs",
   170  					Locations: []string{"mnt/stateful_partition/var_overlay/db/pkg/path/to/file-in-cos-pkgs"},
   171  				},
   172  			},
   173  			wantPackages: []*extractor.Package{
   174  				{
   175  					Name:      "file-in-cos-pkgs",
   176  					Locations: []string{"mnt/stateful_partition/var_overlay/db/pkg/path/to/file-in-cos-pkgs"},
   177  					// No exploitability signals
   178  				},
   179  			},
   180  			wantErr: cmpopts.AnyError,
   181  		},
   182  	}
   183  
   184  	for _, tt := range tests {
   185  		t.Run(tt.desc, func(t *testing.T) {
   186  			if tt.ctx == nil {
   187  				tt.ctx = t.Context()
   188  			}
   189  			input := tt.input
   190  			if input == nil {
   191  				input = &annotator.ScanInput{
   192  					ScanRoot: mustCOSFS(t),
   193  				}
   194  			}
   195  
   196  			// Deep copy the packages to avoid modifying the original inventory that is used in other tests.
   197  			packages := copier.Copy(tt.packages).([]*extractor.Package)
   198  			inv := &inventory.Inventory{Packages: packages}
   199  
   200  			err := cos.New().Annotate(tt.ctx, input, inv)
   201  			if !cmp.Equal(tt.wantErr, err, cmpopts.EquateErrors()) {
   202  				t.Fatalf("Annotate(%v) error: %v, want %v", tt.packages, err, tt.wantErr)
   203  			}
   204  
   205  			want := &inventory.Inventory{Packages: tt.wantPackages}
   206  			if diff := cmp.Diff(want, inv); diff != "" {
   207  				t.Errorf("Annotate(%v): unexpected diff (-want +got): %v", tt.packages, diff)
   208  			}
   209  		})
   210  	}
   211  }
   212  
   213  // mustWriteFiles creates all directories and writes all files in the given map.
   214  func mustWriteFiles(t *testing.T, files map[string]string) {
   215  	t.Helper()
   216  	for path, content := range files {
   217  		if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
   218  			t.Fatalf("Failed to create directory %s: %v", filepath.Dir(path), err)
   219  		}
   220  		if err := os.WriteFile(path, []byte(content), 0644); err != nil {
   221  			t.Fatalf("Failed to write file %s: %v", path, err)
   222  		}
   223  	}
   224  }
   225  
   226  // mustCOSFS returns a ScanRoot representing a COS filesystem with the package info file.
   227  func mustCOSFS(t *testing.T) *scalibrfs.ScanRoot {
   228  	t.Helper()
   229  	dir := t.TempDir()
   230  	files := map[string]string{
   231  		filepath.Join(dir, cosPackageInfoFile): "",
   232  	}
   233  	mustWriteFiles(t, files)
   234  	return scalibrfs.RealFSScanRoot(dir)
   235  }