package cmd import ( "io/ioutil" "os" "os/exec" "path" "path/filepath" "strconv" "strings" "time" "github.com/SAP/jenkins-library/pkg/command" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" FileUtils "github.com/SAP/jenkins-library/pkg/piperutils" SliceUtils "github.com/SAP/jenkins-library/pkg/piperutils" StepResults "github.com/SAP/jenkins-library/pkg/piperutils" SonarUtils "github.com/SAP/jenkins-library/pkg/sonar" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/pkg/errors" ) type sonarSettings struct { workingDir string binary string environment []string options []string } func (s *sonarSettings) addEnvironment(element string) { s.environment = append(s.environment, element) } func (s *sonarSettings) addOption(element string) { s.options = append(s.options, element) } var sonar sonarSettings var execLookPath = exec.LookPath var fileUtilsExists = FileUtils.FileExists var fileUtilsUnzip = FileUtils.Unzip var osRename = os.Rename func sonarExecuteScan(config sonarExecuteScanOptions, _ *telemetry.CustomData) { runner := command.Command{} // reroute command output to logging framework runner.Stdout(log.Entry().Writer()) runner.Stderr(log.Entry().Writer()) client := piperhttp.Client{} client.SetOptions(piperhttp.ClientOptions{TransportTimeout: 20 * time.Second}) sonar = sonarSettings{ workingDir: "./", binary: "sonar-scanner", environment: []string{}, options: []string{}, } if err := runSonar(config, &client, &runner); err != nil { log.Entry().WithError(err).Fatal("Execution failed") } } func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runner execRunner) error { if len(config.Host) > 0 { sonar.addEnvironment("SONAR_HOST_URL=" + config.Host) } if len(config.Token) > 0 { sonar.addEnvironment("SONAR_TOKEN=" + config.Token) } if len(config.Organization) > 0 { sonar.addOption("sonar.organization=" + config.Organization) } if len(config.ProjectVersion) > 0 { // handleArtifactVersion is reused from cmd/protecodeExecuteScan.go sonar.addOption("sonar.projectVersion=" + handleArtifactVersion(config.ProjectVersion)) } if err := handlePullRequest(config); err != nil { return err } if err := loadSonarScanner(config.SonarScannerDownloadURL, client); err != nil { return err } if err := loadCertificates(config.CustomTLSCertificateLinks, client, runner); err != nil { return err } if len(config.Options) > 0 { sonar.options = append(sonar.options, config.Options...) } sonar.options = SliceUtils.PrefixIfNeeded(SliceUtils.Trim(sonar.options), "-D") log.Entry(). WithField("command", sonar.binary). WithField("options", sonar.options). WithField("environment", sonar.environment). Debug("Executing sonar scan command") // execute scan runner.SetEnv(sonar.environment) err := runner.RunExecutable(sonar.binary, sonar.options...) if err != nil { return err } // load task results taskReport, err := SonarUtils.ReadTaskReport(sonar.workingDir) if err != nil { log.Entry().WithError(err).Warning("Unable to read Sonar task report file.") } else { // write links JSON links := []StepResults.Path{ StepResults.Path{ Target: taskReport.DashboardURL, Name: "Sonar Web UI", }, } StepResults.PersistReportsAndLinks("sonarExecuteScan", sonar.workingDir, nil, links) } return nil } func handlePullRequest(config sonarExecuteScanOptions) error { if len(config.ChangeID) > 0 { if config.LegacyPRHandling { // see https://docs.sonarqube.org/display/PLUG/GitHub+Plugin sonar.addOption("sonar.analysis.mode=preview") sonar.addOption("sonar.github.pullRequest=" + config.ChangeID) if len(config.GithubAPIURL) > 0 { sonar.addOption("sonar.github.endpoint=" + config.GithubAPIURL) } if len(config.GithubToken) > 0 { sonar.addOption("sonar.github.oauth=" + config.GithubToken) } if len(config.Owner) > 0 && len(config.Repository) > 0 { sonar.addOption("sonar.github.repository=" + config.Owner + "/" + config.Repository) } if config.DisableInlineComments { sonar.addOption("sonar.github.disableInlineComments=" + strconv.FormatBool(config.DisableInlineComments)) } } else { // see https://sonarcloud.io/documentation/analysis/pull-request/ provider := strings.ToLower(config.PullRequestProvider) if provider == "github" { if len(config.Owner) > 0 && len(config.Repository) > 0 { sonar.addOption("sonar.pullrequest.github.repository=" + config.Owner + "/" + config.Repository) } } else { return errors.New("Pull-Request provider '" + provider + "' is not supported!") } sonar.addOption("sonar.pullrequest.key=" + config.ChangeID) sonar.addOption("sonar.pullrequest.base=" + config.ChangeTarget) sonar.addOption("sonar.pullrequest.branch=" + config.ChangeBranch) sonar.addOption("sonar.pullrequest.provider=" + provider) } } else if len(config.BranchName) > 0 { sonar.addOption("sonar.branch.name=" + config.BranchName) } return nil } func loadSonarScanner(url string, client piperhttp.Downloader) error { if scannerPath, err := execLookPath(sonar.binary); err == nil { // using existing sonar-scanner log.Entry().WithField("path", scannerPath).Debug("Using local sonar-scanner") } else if len(url) != 0 { // download sonar-scanner-cli into TEMP folder log.Entry().WithField("url", url).Debug("Downloading sonar-scanner") tmpFolder := getTempDir() defer os.RemoveAll(tmpFolder) // clean up archive := filepath.Join(tmpFolder, path.Base(url)) if err := client.DownloadFile(url, archive, nil, nil); err != nil { return errors.Wrap(err, "Download of sonar-scanner failed") } // unzip sonar-scanner-cli log.Entry().WithField("source", archive).WithField("target", tmpFolder).Debug("Extracting sonar-scanner") if _, err := fileUtilsUnzip(archive, tmpFolder); err != nil { return errors.Wrap(err, "Extraction of sonar-scanner failed") } // move sonar-scanner-cli to .sonar-scanner/ toolPath := ".sonar-scanner" foldername := strings.ReplaceAll(strings.ReplaceAll(archive, ".zip", ""), "cli-", "") log.Entry().WithField("source", foldername).WithField("target", toolPath).Debug("Moving sonar-scanner") if err := osRename(foldername, toolPath); err != nil { return errors.Wrap(err, "Moving of sonar-scanner failed") } // update binary path sonar.binary = filepath.Join(getWorkingDir(), toolPath, "bin", sonar.binary) log.Entry().Debug("Download completed") } return nil } func loadCertificates(certificateString string, client piperhttp.Downloader, runner execRunner) error { trustStoreFile := filepath.Join(getWorkingDir(), ".certificates", "cacerts") if exists, _ := fileUtilsExists(trustStoreFile); exists { // use local existing trust store sonar.addEnvironment("SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore=" + trustStoreFile) log.Entry().WithField("trust store", trustStoreFile).Info("Using local trust store") } else //TODO: certificate loading is deactivated due to the missing JAVA keytool // see https://github.com/SAP/jenkins-library/issues/1072 if os.Getenv("PIPER_SONAR_LOAD_CERTIFICATES") == "true" && len(certificateString) > 0 { // use local created trust store with downloaded certificates keytoolOptions := []string{ "-import", "-noprompt", "-storepass", "changeit", "-keystore", trustStoreFile, } tmpFolder := getTempDir() defer os.RemoveAll(tmpFolder) // clean up certificateList := strings.Split(certificateString, ",") for _, certificate := range certificateList { filename := path.Base(certificate) // decode? target := filepath.Join(tmpFolder, filename) log.Entry().WithField("source", certificate).WithField("target", target).Info("Downloading TLS certificate") // download certificate if err := client.DownloadFile(certificate, target, nil, nil); err != nil { return errors.Wrapf(err, "Download of TLS certificate failed") } options := append(keytoolOptions, "-file", target) options = append(options, "-alias", filename) // add certificate to keystore if err := runner.RunExecutable("keytool", options...); err != nil { return errors.Wrap(err, "Adding certificate to keystore failed") } } sonar.addEnvironment("SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore=" + trustStoreFile) log.Entry().WithField("trust store", trustStoreFile).Info("Using local trust store") } else { log.Entry().Debug("Download of TLS certificates skipped") } return nil } func getWorkingDir() string { workingDir, err := os.Getwd() if err != nil { log.Entry().WithError(err).WithField("path", workingDir).Debug("Retrieving of work directory failed") } return workingDir } func getTempDir() string { tmpFolder, err := ioutil.TempDir(".", "temp-") if err != nil { log.Entry().WithError(err).WithField("path", tmpFolder).Debug("Creating temp directory failed") } return tmpFolder }