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  }