github.com/google/osv-scalibr@v0.4.1/binary/scanrunner/scanrunner_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 scanrunner_test 16 17 import ( 18 "bytes" 19 "errors" 20 "io" 21 "os" 22 "path/filepath" 23 "runtime" 24 "slices" 25 "testing" 26 27 "archive/tar" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/go-containerregistry/pkg/v1/empty" 31 "github.com/google/go-containerregistry/pkg/v1/mutate" 32 "github.com/google/go-containerregistry/pkg/v1/tarball" 33 "github.com/google/osv-scalibr/binary/cli" 34 "github.com/google/osv-scalibr/binary/scanrunner" 35 "google.golang.org/protobuf/encoding/prototext" 36 37 spb "github.com/google/osv-scalibr/binary/proto/scan_result_go_proto" 38 ) 39 40 func createDetectorTestFiles(t *testing.T) string { 41 // Create an /etc/passwd file for the example detector. 42 t.Helper() 43 dir := t.TempDir() 44 passwdDir := filepath.Join(dir, "etc") 45 if err := os.Mkdir(passwdDir, 0777); err != nil { 46 t.Fatalf("error creating directory %v: %v", passwdDir, err) 47 } 48 passwdFile := filepath.Join(passwdDir, "passwd") 49 if err := os.WriteFile(passwdFile, []byte("content"), 0644); err != nil { 50 t.Fatalf("Error while creating file %s: %v", passwdFile, err) 51 } 52 return dir 53 } 54 55 func createExtractorTestFiles(t *testing.T) string { 56 // Move an example python metadata file into the test dir for the wheelegg extractor. 57 t.Helper() 58 dir := t.TempDir() 59 distDir := filepath.Join(dir, "pip.dist-info") 60 if err := os.Mkdir(distDir, 0777); err != nil { 61 t.Fatalf("error creating directory %v: %v", distDir, err) 62 } 63 srcFile := "../../extractor/filesystem/language/python/wheelegg/testdata/distinfo_meta" 64 dstFile := filepath.Join(distDir, "METADATA") 65 data, err := os.ReadFile(srcFile) 66 if err != nil { 67 t.Errorf("os.ReadFile(%v): %v", srcFile, err) 68 } 69 if err := os.WriteFile(dstFile, data, 0644); err != nil { 70 t.Fatalf("os.WriteFile(%s): %v", dstFile, err) 71 } 72 return dir 73 } 74 75 func createFailingDetectorTestFiles(t *testing.T) string { 76 // /etc/passwd can't be read. 77 t.Helper() 78 dir := t.TempDir() 79 passwdDir := filepath.Join(dir, "etc") 80 if err := os.Mkdir(passwdDir, 0600); err != nil { 81 t.Fatalf("error creating directory %v: %v", passwdDir, err) 82 } 83 return dir 84 } 85 86 func createImageTarball(t *testing.T) string { 87 t.Helper() 88 89 var buf bytes.Buffer 90 w := tar.NewWriter(&buf) 91 gomod := ` 92 module my-library 93 94 require ( 95 github.com/BurntSushi/toml v1.0.0 96 gopkg.in/yaml.v2 v2.4.0 97 ) 98 ` 99 files := []struct { 100 name, contents string 101 }{ 102 {"go.mod", gomod}, 103 } 104 for _, file := range files { 105 hdr := &tar.Header{ 106 Name: file.name, 107 Mode: 0600, 108 Size: int64(len(file.contents)), 109 Typeflag: tar.TypeReg, 110 } 111 if err := w.WriteHeader(hdr); err != nil { 112 t.Fatalf("couldn't write header for %s: %v", file.name, err) 113 } 114 if _, err := w.Write([]byte(file.contents)); err != nil { 115 t.Fatalf("couldn't write %s: %v", file.name, err) 116 } 117 } 118 w.Close() 119 layer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { 120 return io.NopCloser(bytes.NewBuffer(buf.Bytes())), nil 121 }) 122 if err != nil { 123 t.Fatalf("unable to create layer: %v", err) 124 } 125 image, err := mutate.AppendLayers(empty.Image, layer) 126 if err != nil { 127 t.Fatalf("unable to create image: %v", err) 128 } 129 130 dir := t.TempDir() 131 tarPath := filepath.Join(dir, "image.tar") 132 if err := tarball.WriteToFile(tarPath, nil, image); err != nil { 133 t.Fatalf("unable to write tarball: %v", err) 134 } 135 136 return dir 137 } 138 139 func createBadImageTarball(t *testing.T) string { 140 t.Helper() 141 142 dir := t.TempDir() 143 tarPath := filepath.Join(dir, "image.tar") 144 if err := os.WriteFile(tarPath, []byte("bad tarball"), 0600); err != nil { 145 t.Fatalf("unable to write tarball: %v", err) 146 } 147 return dir 148 } 149 150 func TestRunScan(t *testing.T) { 151 testCases := []struct { 152 desc string 153 setupFunc func(t *testing.T) string 154 flags *cli.Flags 155 wantExit int 156 wantScanStatus spb.ScanStatus_ScanStatusEnum 157 wantPluginStatus []spb.ScanStatus_ScanStatusEnum 158 wantPackagesCount int 159 wantFindingCount int 160 excludeOS []string // test will not run on these operating systems 161 }{ 162 { 163 desc: "Successful detector run", 164 setupFunc: createDetectorTestFiles, 165 flags: &cli.Flags{DetectorsToRun: []string{"cis"}}, 166 wantScanStatus: spb.ScanStatus_SUCCEEDED, 167 wantPluginStatus: []spb.ScanStatus_ScanStatusEnum{spb.ScanStatus_SUCCEEDED}, 168 wantPackagesCount: 0, 169 wantFindingCount: 1, 170 // TODO: b/343368902: Fix once we have a detector for Windows. 171 excludeOS: []string{"windows"}, 172 }, 173 { 174 desc: "Successful extractor run", 175 setupFunc: createExtractorTestFiles, 176 flags: &cli.Flags{ExtractorsToRun: []string{"python/wheelegg"}}, 177 wantScanStatus: spb.ScanStatus_SUCCEEDED, 178 wantPluginStatus: []spb.ScanStatus_ScanStatusEnum{spb.ScanStatus_SUCCEEDED}, 179 wantPackagesCount: 1, 180 wantFindingCount: 0, 181 }, 182 { 183 desc: "Successful image extractor run", 184 setupFunc: createImageTarball, 185 flags: &cli.Flags{ 186 ImageTarball: "image.tar", 187 ExtractorsToRun: []string{"go/gomod"}, 188 }, 189 wantScanStatus: spb.ScanStatus_SUCCEEDED, 190 wantPluginStatus: []spb.ScanStatus_ScanStatusEnum{spb.ScanStatus_SUCCEEDED}, 191 wantPackagesCount: 2, 192 wantFindingCount: 0, 193 }, 194 { 195 desc: "Failure to read image tarball", 196 setupFunc: createBadImageTarball, 197 flags: &cli.Flags{ 198 ImageTarball: "image.tar", 199 ExtractorsToRun: []string{"go/gomod"}, 200 }, 201 wantExit: 1, 202 wantScanStatus: spb.ScanStatus_FAILED, 203 wantPluginStatus: []spb.ScanStatus_ScanStatusEnum{spb.ScanStatus_FAILED}, 204 wantPackagesCount: 0, 205 wantFindingCount: 0, 206 }, 207 { 208 desc: "Unsuccessful plugin run", 209 setupFunc: createFailingDetectorTestFiles, 210 flags: &cli.Flags{DetectorsToRun: []string{"cis"}}, 211 wantExit: 1, 212 wantScanStatus: spb.ScanStatus_PARTIALLY_SUCCEEDED, 213 wantPluginStatus: []spb.ScanStatus_ScanStatusEnum{spb.ScanStatus_FAILED}, 214 wantPackagesCount: 0, 215 wantFindingCount: 0, 216 // TODO: b/343368902: Fix once we have a detector for Windows. 217 excludeOS: []string{"windows"}, 218 }, 219 } 220 221 for _, tc := range testCases { 222 t.Run(tc.desc, func(t *testing.T) { 223 if slices.Contains(tc.excludeOS, runtime.GOOS) { 224 t.Skipf("Skipping test on %s", runtime.GOOS) 225 } 226 227 dir := tc.setupFunc(t) 228 resultFile := filepath.Join(dir, "result.textproto") 229 tc.flags.ResultFile = resultFile 230 231 if tc.flags.ImageTarball != "" { 232 tc.flags.ImageTarball = filepath.Join(dir, tc.flags.ImageTarball) 233 } else { 234 tc.flags.Root = dir 235 } 236 237 gotExit := scanrunner.RunScan(tc.flags) 238 if gotExit != tc.wantExit { 239 t.Fatalf("result.RunScan(%v) = %d, want %d", tc.flags, gotExit, tc.wantExit) 240 } 241 _, err := os.Stat(resultFile) 242 if gotExit == 0 && errors.Is(err, os.ErrNotExist) { 243 t.Fatalf("Scan returned successful exit code 0 but no result file was created") 244 } 245 if gotExit != 0 && errors.Is(err, os.ErrNotExist) { 246 // It is expected that no results are created if the scan fails. Nothing else to check. 247 return 248 } 249 250 output, err := os.ReadFile(resultFile) 251 if err != nil { 252 t.Fatalf("os.ReadFile(%v): %v", resultFile, err) 253 } 254 255 result := &spb.ScanResult{} 256 if err = prototext.Unmarshal(output, result); err != nil { 257 t.Fatalf("prototext.Unmarshal(%v): %v", result, err) 258 } 259 if result.Status.Status != tc.wantScanStatus { 260 t.Errorf("Unexpected scan status, want %v got %v", tc.wantScanStatus, result.Status.Status) 261 } 262 gotPS := []spb.ScanStatus_ScanStatusEnum{} 263 for _, s := range result.PluginStatus { 264 gotPS = append(gotPS, s.Status.Status) 265 } 266 if diff := cmp.Diff(tc.wantPluginStatus, gotPS); diff != "" { 267 t.Errorf("Unexpected plugin status (-want +got):\n%s", diff) 268 } 269 if len(result.Inventory.Packages) != tc.wantPackagesCount { 270 t.Errorf("Unexpected package count, want %d got %d", tc.wantPackagesCount, len(result.Inventory.Packages)) 271 } 272 if len(result.Inventory.GenericFindings) != tc.wantFindingCount { 273 t.Errorf("Unexpected finding count, want %d got %d", tc.wantFindingCount, len(result.Inventory.GenericFindings)) 274 } 275 }) 276 } 277 }