github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/snap/snap_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 snap_test 16 17 import ( 18 "fmt" 19 "io/fs" 20 "os" 21 "path/filepath" 22 "runtime" 23 "slices" 24 "testing" 25 26 "github.com/google/go-cmp/cmp" 27 "github.com/google/go-cmp/cmp/cmpopts" 28 "github.com/google/osv-scalibr/extractor" 29 "github.com/google/osv-scalibr/extractor/filesystem" 30 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 31 "github.com/google/osv-scalibr/extractor/filesystem/os/snap" 32 snapmeta "github.com/google/osv-scalibr/extractor/filesystem/os/snap/metadata" 33 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 34 scalibrfs "github.com/google/osv-scalibr/fs" 35 "github.com/google/osv-scalibr/inventory" 36 "github.com/google/osv-scalibr/purl" 37 "github.com/google/osv-scalibr/stats" 38 "github.com/google/osv-scalibr/testing/fakefs" 39 "github.com/google/osv-scalibr/testing/testcollector" 40 ) 41 42 const DebianBookworm = `PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" 43 NAME="Debian GNU/Linux" 44 VERSION_ID="12" 45 VERSION="12 (bookworm)" 46 VERSION_CODENAME=bookworm 47 ID=debian` 48 49 func TestFileRequired(t *testing.T) { 50 // supported OSes 51 if !slices.Contains([]string{"linux"}, runtime.GOOS) { 52 t.Skipf("Test skipped, OS unsupported: %v", runtime.GOOS) 53 } 54 55 tests := []struct { 56 name string 57 path string 58 fileSizeBytes int64 59 maxFileSizeBytes int64 60 wantRequired bool 61 wantResultMetric stats.FileRequiredResult 62 }{ 63 { 64 name: "package info", 65 path: "snap/core/current/meta/snap.yaml", 66 wantRequired: true, 67 wantResultMetric: stats.FileRequiredResultOK, 68 }, { 69 name: "not a snap yaml file", 70 path: "some/other/file.yaml", 71 wantRequired: false, 72 }, { 73 name: "missing revision in path", 74 path: "snap/core/meta/snap.yaml", 75 wantRequired: false, 76 }, { 77 name: "missing name in path", 78 path: "snap/current/meta/snap.yaml", 79 wantRequired: false, 80 }, { 81 name: "extra dirs in path", 82 path: "snap/core/current/extra/meta/snap.yaml", 83 wantRequired: false, 84 }, { 85 name: "snap.yaml required if file size < max file size", 86 path: "snap/core/current/meta/snap.yaml", 87 fileSizeBytes: 100 * units.KiB, 88 maxFileSizeBytes: 1000 * units.KiB, 89 wantRequired: true, 90 wantResultMetric: stats.FileRequiredResultOK, 91 }, { 92 name: "snap.yaml required if file size == max file size", 93 path: "snap/core/current/meta/snap.yaml", 94 fileSizeBytes: 1000 * units.KiB, 95 maxFileSizeBytes: 1000 * units.KiB, 96 wantRequired: true, 97 wantResultMetric: stats.FileRequiredResultOK, 98 }, { 99 name: "snap.yaml not required if file size > max file size", 100 path: "snap/core/current/meta/snap.yaml", 101 fileSizeBytes: 1000 * units.KiB, 102 maxFileSizeBytes: 100 * units.KiB, 103 wantRequired: false, 104 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 105 }, { 106 name: "snap.yaml required if max file size set to 0", 107 path: "snap/core/current/meta/snap.yaml", 108 fileSizeBytes: 100 * units.KiB, 109 maxFileSizeBytes: 0, 110 wantRequired: true, 111 wantResultMetric: stats.FileRequiredResultOK, 112 }, 113 } 114 115 for _, tt := range tests { 116 t.Run(tt.name, func(t *testing.T) { 117 collector := testcollector.New() 118 var e filesystem.Extractor = snap.New(snap.Config{ 119 Stats: collector, 120 MaxFileSizeBytes: tt.maxFileSizeBytes, 121 }) 122 123 // Set a default file size if not specified. 124 fileSizeBytes := tt.fileSizeBytes 125 if fileSizeBytes == 0 { 126 fileSizeBytes = 1000 127 } 128 129 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 130 FileName: filepath.Base(tt.path), 131 FileMode: fs.ModePerm, 132 FileSize: fileSizeBytes, 133 })) 134 if isRequired != tt.wantRequired { 135 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 136 } 137 138 gotResultMetric := collector.FileRequiredResult(tt.path) 139 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 140 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 141 } 142 }) 143 } 144 } 145 146 func TestExtract(t *testing.T) { 147 // supported OSes 148 if !slices.Contains([]string{"linux"}, runtime.GOOS) { 149 t.Skipf("Test skipped, OS unsupported: %v", runtime.GOOS) 150 } 151 152 tests := []struct { 153 name string 154 path string 155 osrelease string 156 wantPackages []*extractor.Package 157 wantErr error 158 wantResultMetric stats.FileExtractedResult 159 }{ 160 { 161 name: "invalid", 162 path: "testdata/invalid", 163 osrelease: DebianBookworm, 164 wantErr: cmpopts.AnyError, 165 wantResultMetric: stats.FileExtractedResultErrorUnknown, 166 }, 167 { 168 name: "valid yaml with single arch", 169 path: "testdata/single-arch.yaml", 170 osrelease: DebianBookworm, 171 wantPackages: []*extractor.Package{ 172 { 173 Name: "core", 174 Version: "16-2.61.4-20240607", 175 PURLType: purl.TypeSnap, 176 Locations: []string{"testdata/single-arch.yaml"}, 177 Metadata: &snapmeta.Metadata{ 178 Name: "core", 179 Version: "16-2.61.4-20240607", 180 Grade: "stable", 181 Type: "os", 182 Architectures: []string{"amd64"}, 183 OSID: "debian", 184 OSVersionCodename: "bookworm", 185 OSVersionID: "12", 186 }, 187 }, 188 }, 189 wantResultMetric: stats.FileExtractedResultSuccess, 190 }, 191 { 192 name: "valid yaml with multiple arch", 193 path: "testdata/multi-arch.yaml", 194 osrelease: DebianBookworm, 195 wantPackages: []*extractor.Package{ 196 { 197 Name: "core", 198 Version: "16-2.61.4-20240607", 199 PURLType: purl.TypeSnap, 200 Locations: []string{"testdata/multi-arch.yaml"}, 201 Metadata: &snapmeta.Metadata{ 202 Name: "core", 203 Version: "16-2.61.4-20240607", 204 Grade: "stable", 205 Type: "os", 206 Architectures: []string{"amd64", "arm64"}, 207 OSID: "debian", 208 OSVersionCodename: "bookworm", 209 OSVersionID: "12", 210 }, 211 }, 212 }, 213 wantResultMetric: stats.FileExtractedResultSuccess, 214 }, 215 { 216 name: "yaml missing name", 217 path: "testdata/missing-name.yaml", 218 osrelease: DebianBookworm, 219 wantErr: cmpopts.AnyError, 220 wantResultMetric: stats.FileExtractedResultErrorUnknown, 221 }, 222 { 223 name: "yaml missing version", 224 path: "testdata/missing-version.yaml", 225 osrelease: DebianBookworm, 226 wantErr: cmpopts.AnyError, 227 wantResultMetric: stats.FileExtractedResultErrorUnknown, 228 }, 229 } 230 231 for _, tt := range tests { 232 t.Run(tt.name, func(t *testing.T) { 233 collector := testcollector.New() 234 var e filesystem.Extractor = snap.New(snap.Config{ 235 Stats: collector, 236 }) 237 238 d := t.TempDir() 239 createOsRelease(t, d, tt.osrelease) 240 241 r, err := os.Open(tt.path) 242 defer func() { 243 if err = r.Close(); err != nil { 244 t.Errorf("Close(): %v", err) 245 } 246 }() 247 if err != nil { 248 t.Fatal(err) 249 } 250 251 info, err := os.Stat(tt.path) 252 if err != nil { 253 t.Fatalf("Failed to stat test file: %v", err) 254 } 255 256 input := &filesystem.ScanInput{ 257 FS: scalibrfs.DirFS(d), 258 Path: tt.path, 259 Reader: r, 260 Root: d, 261 Info: info, 262 } 263 264 got, err := e.Extract(t.Context(), input) 265 if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) { 266 t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.path, err, tt.wantErr) 267 } 268 269 ignoreOrder := cmpopts.SortSlices(func(a, b any) bool { 270 return fmt.Sprintf("%+v", a) < fmt.Sprintf("%+v", b) 271 }) 272 wantInv := inventory.Inventory{Packages: tt.wantPackages} 273 if diff := cmp.Diff(wantInv, got, ignoreOrder); diff != "" { 274 t.Errorf("Extract(%s) (-want +got):\n%s", tt.path, diff) 275 } 276 277 gotResultMetric := collector.FileExtractedResult(tt.path) 278 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 279 t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 280 } 281 282 gotFileSizeMetric := collector.FileExtractedFileSize(tt.path) 283 if gotFileSizeMetric != info.Size() { 284 t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.path, gotFileSizeMetric, info.Size()) 285 } 286 }) 287 } 288 } 289 290 func createOsRelease(t *testing.T, root string, content string) { 291 t.Helper() 292 _ = os.MkdirAll(filepath.Join(root, "etc"), 0755) 293 err := os.WriteFile(filepath.Join(root, "etc/os-release"), []byte(content), 0644) 294 if err != nil { 295 t.Fatalf("write to %s: %v\n", filepath.Join(root, "etc/os-release"), err) 296 } 297 }