github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/kernel/module/module_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 module_test 16 17 import ( 18 "io/fs" 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/osv-scalibr/extractor" 26 "github.com/google/osv-scalibr/extractor/filesystem" 27 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 28 "github.com/google/osv-scalibr/extractor/filesystem/os/kernel/module" 29 modulemeta "github.com/google/osv-scalibr/extractor/filesystem/os/kernel/module/metadata" 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/stats" 34 "github.com/google/osv-scalibr/testing/fakefs" 35 "github.com/google/osv-scalibr/testing/testcollector" 36 ) 37 38 func TestNew(t *testing.T) { 39 tests := []struct { 40 name string 41 cfg module.Config 42 wantCfg module.Config 43 }{ 44 { 45 name: "default", 46 cfg: module.DefaultConfig(), 47 wantCfg: module.Config{ 48 MaxFileSizeBytes: 100 * units.MiB, 49 }, 50 }, 51 { 52 name: "custom", 53 cfg: module.Config{ 54 MaxFileSizeBytes: 10, 55 }, 56 wantCfg: module.Config{ 57 MaxFileSizeBytes: 10, 58 }, 59 }, 60 } 61 62 for _, tt := range tests { 63 t.Run(tt.name, func(t *testing.T) { 64 got := module.New(tt.cfg) 65 if diff := cmp.Diff(tt.wantCfg, got.Config()); diff != "" { 66 t.Errorf("New(%+v).Config(): (-want +got):\n%s", tt.cfg, diff) 67 } 68 }) 69 } 70 } 71 72 func TestFileRequired(t *testing.T) { 73 tests := []struct { 74 name string 75 path string 76 fileSizeBytes int64 77 maxFileSizeBytes int64 78 wantRequired bool 79 wantResultMetric stats.FileRequiredResult 80 }{ 81 { 82 name: "required *.ko file", 83 path: "/usr/lib/modules/6.8.0-48-generic/kernel/arch/x86/crypto/cast5-avx-x86_64.ko", 84 wantRequired: true, 85 wantResultMetric: stats.FileRequiredResultOK, 86 }, 87 { 88 name: "file required if file size < max file size", 89 path: "/usr/lib/modules/6.8.0-48-generic/kernel/arch/x86/crypto/cast5-avx-x86_64.ko", 90 fileSizeBytes: 100 * units.KiB, 91 maxFileSizeBytes: 1000 * units.KiB, 92 wantRequired: true, 93 wantResultMetric: stats.FileRequiredResultOK, 94 }, 95 { 96 name: "file required if file size == max file size", 97 path: "/usr/lib/modules/6.8.0-48-generic/kernel/arch/x86/crypto/cast5-avx-x86_64.ko", 98 fileSizeBytes: 1000 * units.KiB, 99 maxFileSizeBytes: 1000 * units.KiB, 100 wantRequired: true, 101 wantResultMetric: stats.FileRequiredResultOK, 102 }, 103 { 104 name: "file not required if file size > max file size", 105 path: "/usr/lib/modules/6.8.0-48-generic/kernel/arch/x86/crypto/cast5-avx-x86_64.ko", 106 fileSizeBytes: 1000 * units.KiB, 107 maxFileSizeBytes: 100 * units.KiB, 108 wantRequired: false, 109 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 110 }, 111 { 112 name: "file required if max file size set to 0", 113 path: "/usr/lib/modules/6.8.0-48-generic/kernel/arch/x86/crypto/cast5-avx-x86_64.ko", 114 fileSizeBytes: 100 * units.KiB, 115 maxFileSizeBytes: 0, 116 wantRequired: true, 117 wantResultMetric: stats.FileRequiredResultOK, 118 }, 119 { 120 name: "not required", 121 path: "/usr/lib/modules/6.8.0-48-generic/kernel/arch/x86/crypto/cast5-avx-x86_64.o", 122 wantRequired: false, 123 }, 124 } 125 126 for _, tt := range tests { 127 t.Run(tt.name, func(t *testing.T) { 128 collector := testcollector.New() 129 var e filesystem.Extractor = module.New(module.Config{ 130 Stats: collector, 131 MaxFileSizeBytes: tt.maxFileSizeBytes, 132 }) 133 134 fileSizeBytes := tt.fileSizeBytes 135 if fileSizeBytes == 0 { 136 fileSizeBytes = 1000 137 } 138 139 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 140 FileName: filepath.Base(tt.path), 141 FileMode: fs.ModePerm, 142 FileSize: fileSizeBytes, 143 })) 144 if isRequired != tt.wantRequired { 145 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 146 } 147 148 gotResultMetric := collector.FileRequiredResult(tt.path) 149 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 150 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 151 } 152 }) 153 } 154 } 155 156 const UbuntuJammy = `PRETTY_NAME="Ubuntu 22.04.5 LTS" 157 NAME="Ubuntu" 158 VERSION_ID="22.04" 159 VERSION="22.04.5 LTS (Jammy Jellyfish)" 160 VERSION_CODENAME=jammy 161 ID=ubuntu 162 ID_LIKE=debian 163 HOME_URL="https://www.ubuntu.com/" 164 SUPPORT_URL="https://help.ubuntu.com/" 165 BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" 166 PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" 167 UBUNTU_CODENAME=jammy 168 ` 169 170 func TestExtract(t *testing.T) { 171 tests := []struct { 172 name string 173 path string 174 osrelease string 175 cfg module.Config 176 wantPackages []*extractor.Package 177 wantErr error 178 wantResultMetric stats.FileExtractedResult 179 }{ 180 { 181 name: "valid *.ko file", 182 path: "testdata/valid", 183 osrelease: UbuntuJammy, 184 wantPackages: []*extractor.Package{ 185 { 186 Name: "intel_oaktrail", 187 Version: "0.4ac1", 188 Metadata: &modulemeta.Metadata{ 189 PackageName: "intel_oaktrail", 190 PackageVersion: "0.4ac1", 191 PackageVermagic: "6.5.0-45-generic SMP preempt mod_unload modversions", 192 PackageSourceVersionIdentifier: "69B4F4432F52708A284377E", 193 OSID: "ubuntu", 194 OSVersionCodename: "jammy", 195 OSVersionID: "22.04", 196 PackageAuthor: "Yin Kangkai (kangkai.yin@intel.com)", 197 }, 198 Locations: []string{"testdata/valid"}, 199 }, 200 }, 201 wantResultMetric: stats.FileExtractedResultSuccess, 202 }, 203 { 204 name: "valid *.ko file without version, deps, author", 205 path: "testdata/valid_no_vers_deps_auth", 206 osrelease: UbuntuJammy, 207 wantPackages: []*extractor.Package{ 208 { 209 Name: "intel_mrfld_pwrbtn", 210 Metadata: &modulemeta.Metadata{ 211 PackageName: "intel_mrfld_pwrbtn", 212 PackageVermagic: "6.8.0-49-generic SMP preempt mod_unload modversions", 213 PackageSourceVersionIdentifier: "F64DA2CCFC87C17684B7B8B", 214 OSID: "ubuntu", 215 OSVersionCodename: "jammy", 216 OSVersionID: "22.04", 217 }, 218 Locations: []string{"testdata/valid_no_vers_deps_auth"}, 219 }, 220 }, 221 wantResultMetric: stats.FileExtractedResultSuccess, 222 }, 223 { 224 name: "invalid *.ko file, no .modinfo section", 225 path: "testdata/invalid", 226 osrelease: UbuntuJammy, 227 wantPackages: nil, 228 wantErr: cmpopts.AnyError, 229 wantResultMetric: stats.FileExtractedResultErrorUnknown, 230 }, 231 { 232 name: "no os version", 233 path: "testdata/valid", 234 osrelease: `ID=ubuntu`, 235 wantPackages: []*extractor.Package{ 236 { 237 Name: "intel_oaktrail", 238 Version: "0.4ac1", 239 Metadata: &modulemeta.Metadata{ 240 PackageName: "intel_oaktrail", 241 PackageVersion: "0.4ac1", 242 PackageVermagic: "6.5.0-45-generic SMP preempt mod_unload modversions", 243 PackageSourceVersionIdentifier: "69B4F4432F52708A284377E", 244 OSID: "ubuntu", 245 PackageAuthor: "Yin Kangkai (kangkai.yin@intel.com)", 246 }, 247 Locations: []string{"testdata/valid"}, 248 }, 249 }, 250 wantResultMetric: stats.FileExtractedResultSuccess, 251 }, 252 { 253 name: "missing_osrelease", 254 path: "testdata/valid", 255 wantPackages: []*extractor.Package{ 256 { 257 Name: "intel_oaktrail", 258 Version: "0.4ac1", 259 Metadata: &modulemeta.Metadata{ 260 PackageName: "intel_oaktrail", 261 PackageVersion: "0.4ac1", 262 PackageVermagic: "6.5.0-45-generic SMP preempt mod_unload modversions", 263 PackageSourceVersionIdentifier: "69B4F4432F52708A284377E", 264 PackageAuthor: "Yin Kangkai (kangkai.yin@intel.com)", 265 }, 266 Locations: []string{"testdata/valid"}, 267 }, 268 }, 269 wantResultMetric: stats.FileExtractedResultSuccess, 270 }, 271 } 272 273 for _, tt := range tests { 274 t.Run(tt.name, func(t *testing.T) { 275 collector := testcollector.New() 276 var e filesystem.Extractor = module.New(module.Config{ 277 Stats: collector, 278 MaxFileSizeBytes: 100, 279 }) 280 281 d := t.TempDir() 282 createOsRelease(t, d, tt.osrelease) 283 284 // Opening and Reading the Test File 285 r, err := os.Open(tt.path) 286 defer func() { 287 if err = r.Close(); err != nil { 288 t.Errorf("Close(): %v", err) 289 } 290 }() 291 if err != nil { 292 t.Fatal(err) 293 } 294 295 info, err := os.Stat(tt.path) 296 if err != nil { 297 t.Fatalf("Failed to stat test file: %v", err) 298 } 299 300 input := &filesystem.ScanInput{ 301 FS: scalibrfs.DirFS(d), Path: tt.path, Reader: r, Root: d, Info: info, 302 } 303 304 got, err := e.Extract(t.Context(), input) 305 306 wantInv := inventory.Inventory{Packages: tt.wantPackages} 307 if diff := cmp.Diff(wantInv, got); diff != "" { 308 t.Errorf("Package mismatch (-want +got):\n%s", diff) 309 } 310 311 if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) { 312 t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.path, err, tt.wantErr) 313 } 314 }) 315 } 316 } 317 318 func createOsRelease(t *testing.T, root string, content string) { 319 t.Helper() 320 _ = os.MkdirAll(filepath.Join(root, "etc"), 0755) 321 err := os.WriteFile(filepath.Join(root, "etc/os-release"), []byte(content), 0644) 322 if err != nil { 323 t.Fatalf("write to %s: %v\n", filepath.Join(root, "etc/os-release"), err) 324 } 325 }