github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/macapps/macapps_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 macapps_test 16 17 import ( 18 "fmt" 19 "io/fs" 20 "os" 21 "path/filepath" 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/internal/units" 29 "github.com/google/osv-scalibr/extractor/filesystem/os/macapps" 30 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 31 scalibrfs "github.com/google/osv-scalibr/fs" 32 "github.com/google/osv-scalibr/inventory" 33 "github.com/google/osv-scalibr/purl" 34 "github.com/google/osv-scalibr/stats" 35 "github.com/google/osv-scalibr/testing/fakefs" 36 "github.com/google/osv-scalibr/testing/testcollector" 37 ) 38 39 func TestFileRequired(t *testing.T) { 40 tests := []struct { 41 name string 42 path string 43 fileSizeBytes int64 44 maxFileSizeBytes int64 45 wantRequired bool 46 wantResultMetric stats.FileRequiredResult 47 }{ 48 { 49 name: "Valid_File_Path_for_Info.plist", 50 path: "Applications/GoogleChrome.app/Contents/Info.plist", 51 wantRequired: true, 52 wantResultMetric: stats.FileRequiredResultOK, 53 }, 54 { 55 name: "Invalid_Prefix_for_Info.plist", 56 path: "/testdata/Applications/GoogleChrome.app/Contents/Info.plist", 57 wantRequired: false, 58 }, 59 { 60 name: "Invalid_Suffix_for_Info.plist", 61 path: "Applications/GoogleChrome.app/Contents/Info.plists", 62 wantRequired: false, 63 }, 64 { 65 name: "InvalidMiddle_for_Info.plist", 66 path: "Applications/GoogleChrome.app/Info.plist", 67 wantRequired: false, 68 }, 69 { 70 name: "no_sub_packages", 71 path: "Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/Info.plist", 72 wantRequired: false, 73 }, 74 { 75 name: "Info.plist_file_required_if_file_size<max_file_size", 76 path: "Applications/LargeApp/Contents/Info.plist", 77 fileSizeBytes: 100 * units.KiB, 78 maxFileSizeBytes: 1 * units.MiB, 79 wantRequired: true, 80 wantResultMetric: stats.FileRequiredResultOK, 81 }, 82 { 83 name: "Info.plist_file_required_if_file_size==max_file_size", 84 path: "Applications/LargeApp/Contents/Info.plist", 85 fileSizeBytes: 1 * units.MiB, 86 maxFileSizeBytes: 1 * units.MiB, 87 wantRequired: true, 88 wantResultMetric: stats.FileRequiredResultOK, 89 }, 90 { 91 name: "Info.plist_file_not_required_if_file_size>max_file_size", 92 path: "Applications/LargeApp/Contents/Info.plist", 93 fileSizeBytes: 10 * units.MiB, 94 maxFileSizeBytes: 1 * units.MiB, 95 wantRequired: false, 96 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 97 }, 98 } 99 100 for _, tt := range tests { 101 t.Run(tt.name, func(t *testing.T) { 102 collector := testcollector.New() 103 e := macapps.New(macapps.Config{ 104 Stats: collector, 105 MaxFileSizeBytes: tt.maxFileSizeBytes, 106 }) 107 108 // Set a default file size if not specified. 109 fileSizeBytes := tt.fileSizeBytes 110 if fileSizeBytes == 0 { 111 fileSizeBytes = 1000 112 } 113 114 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 115 FileName: filepath.Base(tt.path), 116 FileMode: fs.ModePerm, 117 FileSize: fileSizeBytes, 118 })) 119 if isRequired != tt.wantRequired { 120 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 121 } 122 123 gotResultMetric := collector.FileRequiredResult(tt.path) 124 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 125 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 126 } 127 }) 128 } 129 } 130 131 func TestExtract(t *testing.T) { 132 tests := []struct { 133 name string 134 path string 135 wantPackages []*extractor.Package 136 wantErr error 137 wantResultMetric stats.FileExtractedResult 138 }{ 139 { 140 name: "Valid_XML_Info.plist_data_", 141 path: "testdata/ValidXML.plist", 142 wantPackages: []*extractor.Package{ 143 &extractor.Package{ 144 Name: "Chrome", 145 Version: "130.0.6723.69", 146 PURLType: purl.TypeMacApps, 147 Locations: []string{"testdata/ValidXML.plist"}, 148 Metadata: &macapps.Metadata{ 149 CFBundleDisplayName: "Google Chrome", 150 CFBundleIdentifier: "com.google.Chrome", 151 CFBundleShortVersionString: "130.0.6723.69", 152 CFBundleExecutable: "Google Chrome", 153 CFBundleName: "Chrome", 154 CFBundlePackageType: "APPL", 155 CFBundleSignature: "rimZ", 156 CFBundleVersion: "6723.69", 157 KSProductID: "com.google.Chrome", 158 KSUpdateURL: "https://tools.google.com/service/update2", 159 }, 160 }, 161 }, 162 wantResultMetric: stats.FileExtractedResultSuccess, 163 }, 164 { 165 name: "Valid_Binary_Info.plist_data_", 166 path: "testdata/BinaryApp.plist", 167 wantPackages: []*extractor.Package{ 168 &extractor.Package{ 169 Name: "gMacInformation", 170 Version: "202410231131", 171 PURLType: purl.TypeMacApps, 172 Locations: []string{"testdata/BinaryApp.plist"}, 173 Metadata: &macapps.Metadata{ 174 CFBundleDisplayName: "", 175 CFBundleIdentifier: "com.google.corp.gMacInformation", 176 CFBundleShortVersionString: "202410231131", 177 CFBundleExecutable: "gMacInformation", 178 CFBundleName: "gMacInformation", 179 CFBundlePackageType: "APPL", 180 CFBundleSignature: "????", 181 CFBundleVersion: "202410231131", 182 KSProductID: "", 183 KSUpdateURL: "", 184 }, 185 }, 186 }, 187 wantResultMetric: stats.FileExtractedResultSuccess, 188 }, 189 { 190 name: "Empty_Info.plist_data ", 191 path: "testdata/Empty.plist", 192 wantErr: cmpopts.AnyError, 193 wantResultMetric: stats.FileExtractedResultErrorUnknown, 194 }, 195 { 196 name: "Invalid_format_Info.plist_data ", 197 path: "testdata/InvalidFormat.plist", 198 wantErr: cmpopts.AnyError, 199 wantResultMetric: stats.FileExtractedResultErrorUnknown, 200 }, 201 { 202 name: "Missing_Info.plist_data_", 203 path: "testdata/MissingData.plist", 204 wantPackages: []*extractor.Package{ 205 &extractor.Package{ 206 Name: "Chrome", 207 Version: "", 208 PURLType: purl.TypeMacApps, 209 Locations: []string{"testdata/MissingData.plist"}, 210 Metadata: &macapps.Metadata{ 211 CFBundleDisplayName: "", 212 CFBundleIdentifier: "com.google.Chrome", 213 CFBundleShortVersionString: "", 214 CFBundleExecutable: "Google Chrome", 215 CFBundleName: "Chrome", 216 CFBundlePackageType: "APPL", 217 CFBundleSignature: "rimZ", 218 CFBundleVersion: "6723.69", 219 KSProductID: "com.google.Chrome", 220 KSUpdateURL: "https://tools.google.com/service/update2", 221 }, 222 }, 223 }, 224 wantResultMetric: stats.FileExtractedResultSuccess, 225 }, 226 { 227 name: "Different_Format_Info.plist_data ", 228 path: "testdata/DifferentFormat.plist", 229 wantErr: cmpopts.AnyError, 230 wantResultMetric: stats.FileExtractedResultErrorUnknown, 231 }, 232 } 233 234 for _, tt := range tests { 235 t.Run(tt.name, func(t *testing.T) { 236 collector := testcollector.New() 237 e := macapps.New(macapps.Config{ 238 Stats: collector, 239 }) 240 241 d := t.TempDir() 242 243 r, err := os.Open(tt.path) 244 defer func() { 245 if err = r.Close(); err != nil { 246 t.Errorf("Close(): %v", err) 247 } 248 }() 249 if err != nil { 250 t.Fatal(err) 251 } 252 253 info, err := os.Stat(tt.path) 254 if err != nil { 255 t.Fatalf("Failed to stat test file: %v", err) 256 } 257 258 input := &filesystem.ScanInput{ 259 FS: scalibrfs.DirFS(d), 260 Path: tt.path, 261 Reader: r, 262 Root: d, 263 Info: info, 264 } 265 266 got, err := e.Extract(t.Context(), input) 267 if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) { 268 t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.path, err, tt.wantErr) 269 } 270 271 ignoreOrder := cmpopts.SortSlices(func(a, b any) bool { 272 return fmt.Sprintf("%+v", a) < fmt.Sprintf("%+v", b) 273 }) 274 wantInv := inventory.Inventory{Packages: tt.wantPackages} 275 if diff := cmp.Diff(wantInv, got, ignoreOrder); diff != "" { 276 t.Errorf("Extract(%s) (-want +got):\n%s", tt.path, diff) 277 } 278 279 gotResultMetric := collector.FileExtractedResult(tt.path) 280 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 281 t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 282 } 283 284 gotFileSizeMetric := collector.FileExtractedFileSize(tt.path) 285 if gotFileSizeMetric != info.Size() { 286 t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.path, gotFileSizeMetric, info.Size()) 287 } 288 }) 289 } 290 }