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 }