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  }