package cmd import ( "testing" "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "time" pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" "github.com/SAP/jenkins-library/pkg/protecode" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type DockerClientMock struct { imageName string registryURL string localPath string includeLayers bool } //Download interface for download an image to a local path type Download interface { GetImageSource() (string, error) DownloadImageToPath(imageSource, filePath string) (pkgutil.Image, error) TarImage(writer io.Writer, image pkgutil.Image) error } const ( daemonPrefix = "daemon://" remotePrefix = "remote://" ) func (c *DockerClientMock) GetImageSource() (string, error) { imageSource := c.imageName if len(c.registryURL) > 0 && len(c.localPath) <= 0 { registry := c.registryURL url, _ := url.Parse(c.registryURL) //remove protocoll from registryURL to get registry if len(url.Scheme) > 0 { registry = strings.Replace(c.registryURL, fmt.Sprintf("%v://", url.Scheme), "", 1) } if strings.HasSuffix(registry, "/") { imageSource = fmt.Sprintf("%v%v%v", remotePrefix, registry, c.imageName) } else { imageSource = fmt.Sprintf("%v%v/%v", remotePrefix, registry, c.imageName) } } else if len(c.localPath) > 0 { imageSource = c.localPath if !pkgutil.IsTar(c.localPath) { imageSource = fmt.Sprintf("%v%v", daemonPrefix, c.localPath) } } if len(imageSource) <= 0 { return imageSource, fmt.Errorf("There is no image source for the parameters: (Name: %v, Registry: %v, local Path: %v)", c.imageName, c.registryURL, c.localPath) } return imageSource, nil } //DownloadImageToPath download the image to the specified path func (c *DockerClientMock) DownloadImageToPath(imageSource, filePath string) (pkgutil.Image, error) { return pkgutil.Image{}, nil } //TarImage write a tar from the given image func (c *DockerClientMock) TarImage(writer io.Writer, image pkgutil.Image) error { return nil } func TestRunProtecodeScan(t *testing.T) { requestURI := "" dir, err := ioutil.TempDir("", "t") require.NoError(t, err, "Failed to create temporary directory") // clean up tmp dir defer func() { _ = os.RemoveAll(dir) }() testFile, err := ioutil.TempFile(dir, "t.tar") require.NoError(t, err) fileName := filepath.Base(testFile.Name()) path := strings.ReplaceAll(testFile.Name(), fileName, "") server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { requestURI = req.RequestURI var b bytes.Buffer if requestURI == "/api/product/4486/" || requestURI == "/api/product/4711/" { violations := filepath.Join("testdata/TestProtecode", "protecode_result_violations.json") byteContent, err := ioutil.ReadFile(violations) require.NoErrorf(t, err, "failed reading %v", violations) response := protecode.ResultData{Result: protecode.Result{ProductID: 4711, ReportURL: requestURI}} err = json.Unmarshal(byteContent, &response) json.NewEncoder(&b).Encode(response) } else if requestURI == "/api/fetch/" { violations := filepath.Join("testdata/TestProtecode", "protecode_result_violations.json") byteContent, err := ioutil.ReadFile(violations) require.NoErrorf(t, err, "failed reading %v", violations) response := protecode.ResultData{Result: protecode.Result{ProductID: 4486, ReportURL: requestURI}} err = json.Unmarshal(byteContent, &response) json.NewEncoder(&b).Encode(response) } else if requestURI == "/api/product/4486/pdf-report" { } else if requestURI == "/api/upload/t.tar" { response := protecode.ResultData{Result: protecode.Result{ProductID: 4486, ReportURL: requestURI}} var b bytes.Buffer json.NewEncoder(&b).Encode(&response) rw.Write([]byte(b.Bytes())) } else { response := protecode.Result{ProductID: 4486, ReportURL: requestURI} json.NewEncoder(&b).Encode(&response) } rw.Write([]byte(b.Bytes())) })) // Close the server when test finishes defer server.Close() po := protecode.Options{ServerURL: server.URL} pc := protecode.Protecode{} pc.SetOptions(po) dClient := &DockerClientMock{imageName: "t", registryURL: "", localPath: path, includeLayers: false} influx := protecodeExecuteScanInflux{} reportPath = dir cachePath = dir t.Run("With tar as scan image", func(t *testing.T) { config := protecodeExecuteScanOptions{ServerURL: server.URL, TimeoutMinutes: "1", VerifyOnly: false, CleanupMode: "none", Group: "13", FetchURL: "/api/fetch/", ExcludeCVEs: "CVE-2018-1, CVE-2017-1000382", ReportFileName: "./cache/report-file.txt"} err = runProtecodeScan(&config, &influx, dClient) assert.NoError(t, err) }) t.Run("Without tar as scan image", func(t *testing.T) { config := protecodeExecuteScanOptions{ServerURL: server.URL, ScanImage: "t", TimeoutMinutes: "1", VerifyOnly: false, CleanupMode: "none", Group: "13", ExcludeCVEs: "CVE-2018-1, CVE-2017-1000382", ReportFileName: "./cache/report-file.txt"} err = runProtecodeScan(&config, &influx, dClient) assert.NoError(t, err) }) } func TestHandleArtifactVersion(t *testing.T) { cases := []struct { version string want string }{ {"1.0.0-20200131085038+eeb7c1033339bfd404d21ec5e7dc05c80e9e985e", "1"}, {"2.20.20-20200131085038+eeb7c1033339bfd404d21ec5e7dc05c80e9e985e", "2"}, {"3.20.20-20200131085038+eeb7c1033339bfd404d21ec5e7dc05c80e9e985e", "3"}, {"4.20.20-20200131085038", "4"}, {"5.20.20-20200131085038+", "5"}, {"6.00", "6.00"}, {"7.20.20", "7.20.20"}, } for _, c := range cases { got := handleArtifactVersion(c.version) assert.Equal(t, c.want, got) } } func TestCreateClient(t *testing.T) { cases := []struct { timeout string }{ {""}, {"1"}, } for _, c := range cases { config := protecodeExecuteScanOptions{TimeoutMinutes: c.timeout} client := createClient(&config) assert.NotNil(t, client, "client should not be empty") } } func TestCreateDockerClient(t *testing.T) { cases := []struct { scanImage string dockerRegistryURL string filePath string includeLayers bool }{ {"test", "url", "path", false}, {"", "", "", true}, } for _, c := range cases { config := protecodeExecuteScanOptions{ScanImage: c.scanImage, DockerRegistryURL: c.dockerRegistryURL, FilePath: c.filePath, IncludeLayers: c.includeLayers} client := createDockerClient(&config) assert.NotNil(t, client, "client should not be empty") } } func TestUploadScanOrDeclareFetch(t *testing.T) { // init testFile, err := ioutil.TempFile("", "testFileUpload") require.NoError(t, err) defer os.RemoveAll(testFile.Name()) // clean up fileName := filepath.Base(testFile.Name()) path := strings.ReplaceAll(testFile.Name(), fileName, "") requestURI := "" server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { requestURI = req.RequestURI if requestURI == "/api/fetch/" { response := protecode.ResultData{Result: protecode.Result{ProductID: 4711, ReportURL: requestURI}} var b bytes.Buffer json.NewEncoder(&b).Encode(&response) rw.Write([]byte(b.Bytes())) } if requestURI == fmt.Sprintf("/api/upload/%v", fileName) || requestURI == fmt.Sprintf("/api/upload/PR_4711_%v", fileName) { response := protecode.ResultData{Result: protecode.Result{ProductID: 4711, ReportURL: requestURI}} var b bytes.Buffer json.NewEncoder(&b).Encode(&response) rw.Write([]byte(b.Bytes())) } })) // Close the server when test finishes defer server.Close() po := protecode.Options{ServerURL: server.URL} pc := protecode.Protecode{} pc.SetOptions(po) cases := []struct { reuse bool clean string group string fetchURL string filePath string prName string prID int want int }{ {false, "test", "group1", "/api/fetch/", "", "", -1, 4711}, {true, "test", "group1", "/api/fetch/", "", "", -1, 4711}, {false, "test", "group1", "/api/fetch/", "", "", 4711, 4711}, {false, "test", "group1", "/api/fetch/", "", "", 0, 4711}, {false, "test", "group1", "", path, "", -1, 4711}, {true, "test", "group1", "", path, "", -1, 4711}, {false, "test", "group1", "", path, "PR_4711", 4711, 4711}, {false, "test", "group1", "", path, "", 0, 4711}, } for _, c := range cases { // test config := protecodeExecuteScanOptions{VerifyOnly: c.reuse, CleanupMode: c.clean, Group: c.group, FetchURL: c.fetchURL, FilePath: c.filePath} // got := uploadScanOrDeclareFetch(config, 0, pc, fileName) got := uploadScanOrDeclareFetch(config, c.prID, pc, fileName) // assert assert.Equal(t, c.want, got) } } func writeReportToFileMock(resp io.ReadCloser, reportFileName string) error { return nil } func TestExecuteProtecodeScan(t *testing.T) { testDataFile := filepath.Join("testdata", "TestProtecode", "protecode_result_violations.json") violationsAbsPath, err := filepath.Abs(testDataFile) require.NoErrorf(t, err, "failed to obtain absolute path to test data with violations: %v", err) requestURI := "" server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { requestURI = req.RequestURI var b bytes.Buffer if requestURI == "/api/product/4711/" { byteContent, err := ioutil.ReadFile(violationsAbsPath) require.NoErrorf(t, err, "failed reading %v", violationsAbsPath) response := protecode.ResultData{} err = json.Unmarshal(byteContent, &response) json.NewEncoder(&b).Encode(response) } else if requestURI == "/api/product/4711/pdf-report" { } else { response := protecode.ResultData{Result: protecode.Result{ProductID: 4711, ReportURL: requestURI}} json.NewEncoder(&b).Encode(&response) } rw.Write([]byte(b.Bytes())) })) // Close the server when test finishes defer server.Close() po := protecode.Options{ServerURL: server.URL, Duration: time.Minute * 3} pc := protecode.Protecode{} pc.SetOptions(po) cases := []struct { reuse bool clean string group string fetchURL string want int }{ {false, "binary", "group1", "/api/fetch/", 4711}, } resetDir, err := os.Getwd() require.NoErrorf(t, err, "Failed to get current directory: %v", err) defer func() { _ = os.Chdir(resetDir) }() for _, c := range cases { // init dir, err := ioutil.TempDir("", "t") require.NoErrorf(t, err, "Failed to create temporary directory: %v", err) // clean up tmp dir defer func() { _ = os.RemoveAll(dir) }() // change into tmp dir and write test data err = os.Chdir(dir) require.NoErrorf(t, err, "Failed to change into temporary directory: %v", err) reportPath = dir config := protecodeExecuteScanOptions{VerifyOnly: c.reuse, CleanupMode: c.clean, Group: c.group, FetchURL: c.fetchURL, TimeoutMinutes: "3", ExcludeCVEs: "CVE-2018-1, CVE-2017-1000382", ReportFileName: "./cache/report-file.txt"} influxData := &protecodeExecuteScanInflux{} // test executeProtecodeScan(influxData, pc, &config, "dummy", writeReportToFileMock) // assert assert.Equal(t, 1125, influxData.protecode_data.fields.historical_vulnerabilities) assert.Equal(t, 0, influxData.protecode_data.fields.triaged_vulnerabilities) assert.Equal(t, 1, influxData.protecode_data.fields.excluded_vulnerabilities) assert.Equal(t, 142, influxData.protecode_data.fields.major_vulnerabilities) assert.Equal(t, 226, influxData.protecode_data.fields.vulnerabilities) } } func TestCorrectDockerConfigEnvVar(t *testing.T) { t.Run("with credentials", func(t *testing.T) { // init testDirectory, _ := ioutil.TempDir(".", "") require.DirExists(t, testDirectory) defer os.RemoveAll(testDirectory) dockerConfigDir := filepath.Join(testDirectory, "myConfig") os.Mkdir(dockerConfigDir, 0755) require.DirExists(t, dockerConfigDir) dockerConfigFile := filepath.Join(dockerConfigDir, "docker.json") file, _ := os.Create(dockerConfigFile) defer file.Close() require.FileExists(t, dockerConfigFile) resetValue := os.Getenv("DOCKER_CONFIG") defer os.Setenv("DOCKER_CONFIG", resetValue) // test correctDockerConfigEnvVar(&protecodeExecuteScanOptions{DockerConfigJSON: dockerConfigFile}) // assert absolutePath, _ := filepath.Abs(dockerConfigDir) assert.Equal(t, absolutePath, os.Getenv("DOCKER_CONFIG")) }) t.Run("without credentials", func(t *testing.T) { // init resetValue := os.Getenv("DOCKER_CONFIG") defer os.Setenv("DOCKER_CONFIG", resetValue) // test correctDockerConfigEnvVar(&protecodeExecuteScanOptions{}) // assert assert.Equal(t, resetValue, os.Getenv("DOCKER_CONFIG")) }) } func TestGetTarName(t *testing.T) { cases := map[string]struct { image string version string expect string }{ "with version suffix": { "com.sap.piper/sample-k8s-app-multistage:1.11-20200902040158_97a5cc34f1796ad735159f020dd55c0f3670a4cb", "1.11-20200902040158_97a5cc34f1796ad735159f020dd55c0f3670a4cb", "com.sap.piper_sample-k8s-app-multistage_1.tar", }, "without version suffix": { "abc", "3.20.20-20200131085038+eeb7c1033339bfd404d21ec5e7dc05c80e9e985e", "abc_3.tar", }, "without version": { "abc", "", "abc.tar", }, "ScanImage without sha as artifactVersion": { "abc@sha256:12345", "", "abc.tar", }, "ScanImage with sha as artifactVersion": { "ppiper/cf-cli@sha256:c25dbacb9ab6e912afe0fe926d8f9d949c60adfe55d16778bde5941e6c37be11", "c25dbacb9ab6e912afe0fe926d8f9d949c60adfe55d16778bde5941e6c37be11", "ppiper_cf-cli_c25dbacb9ab6e912afe0fe926d8f9d949c60adfe55d16778bde5941e6c37be11.tar", }, } for name, c := range cases { t.Run(name, func(t *testing.T) { assert.Equal(t, c.expect, getTarName(&protecodeExecuteScanOptions{ScanImage: c.image, Version: c.version})) }) } }