github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/cmd/credentialdiggerScan.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strconv"
     8  
     9  	"github.com/SAP/jenkins-library/pkg/command"
    10  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    11  	"github.com/SAP/jenkins-library/pkg/log"
    12  	"github.com/SAP/jenkins-library/pkg/orchestrator"
    13  	"github.com/SAP/jenkins-library/pkg/piperutils"
    14  	"github.com/SAP/jenkins-library/pkg/telemetry"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  const piperDbName string = "piper_step_db.db"
    19  const piperReportName string = "findings.csv"
    20  
    21  type credentialdiggerUtils interface {
    22  	command.ExecRunner
    23  	piperutils.FileUtils
    24  }
    25  
    26  type credentialdiggerUtilsBundle struct {
    27  	*command.Command
    28  	*piperutils.Files
    29  }
    30  
    31  func newCDUtils() credentialdiggerUtils {
    32  	utils := credentialdiggerUtilsBundle{
    33  		Command: &command.Command{},
    34  		Files:   &piperutils.Files{},
    35  	}
    36  	// Reroute command output to logging framework
    37  	utils.Stdout(log.Writer())
    38  	utils.Stderr(log.Writer())
    39  	return &utils
    40  }
    41  
    42  func credentialdiggerScan(config credentialdiggerScanOptions, telemetryData *telemetry.CustomData) error {
    43  	utils := newCDUtils()
    44  	// 0: Get attributes from orchestrator
    45  	provider, prov_err := orchestrator.NewOrchestratorSpecificConfigProvider()
    46  	if prov_err != nil {
    47  		log.Entry().WithError(prov_err).Error(
    48  			"credentialdiggerScan: unable to load orchestrator specific configuration.")
    49  	}
    50  	if config.Repository == "" {
    51  		// Get current repository from orchestrator
    52  		repoUrlOrchestrator := provider.GetRepoURL()
    53  		if repoUrlOrchestrator == "n/a" {
    54  			// Jenkins configuration error
    55  			log.Entry().WithError(errors.New(
    56  				fmt.Sprintf("Unknown repository URL %s", repoUrlOrchestrator))).Error(
    57  				"Repository URL n/a. Please verify git plugin is installed.")
    58  		}
    59  		config.Repository = repoUrlOrchestrator
    60  		log.Entry().Debug("Use current repository: ", repoUrlOrchestrator)
    61  	}
    62  	if provider.IsPullRequest() {
    63  		// set the pr number
    64  		config.PrNumber, _ = strconv.Atoi(provider.GetPullRequestConfig().Key)
    65  		log.Entry().Debug("Scan the current pull request: number ", config.PrNumber)
    66  	}
    67  
    68  	// 1: Add rules
    69  	log.Entry().Info("Load rules")
    70  	err := credentialdiggerAddRules(&config, telemetryData, utils)
    71  	if err != nil {
    72  		log.Entry().Error("credentialdiggerScan: Failed running credentialdigger add_rules")
    73  		return err
    74  	}
    75  	log.Entry().Info("Rules added")
    76  
    77  	// 2: Scan the repository
    78  	// Choose between scan-pr, scan-snapshot, and full-scan (with this priority
    79  	// order)
    80  	switch {
    81  	case config.PrNumber != 0: // int type is not nillable in golang
    82  		log.Entry().Debug("Scan PR")
    83  		// if a PrNumber is declared, run scan_pr
    84  		err = credentialdiggerScanPR(&config, telemetryData, utils) // scan PR with CD
    85  	case config.Snapshot != "":
    86  		log.Entry().Debug("Scan snapshot")
    87  		// if a Snapshot is declared, run scan_snapshot
    88  		err = credentialdiggerScanSnapshot(&config, telemetryData, utils) // scan Snapshot with CD
    89  	default:
    90  		// The default case is the normal full scan
    91  		log.Entry().Debug("Full scan repo")
    92  		err = credentialdiggerFullScan(&config, telemetryData, utils) // full scan with CD
    93  	}
    94  	// err is an error exit number when there are findings
    95  	if err == nil {
    96  		log.Entry().Info("No discoveries found in this repo")
    97  		// If there are no findings, there is no need to export an empty report
    98  		return nil
    99  	}
   100  
   101  	// 3: Get discoveries
   102  	err = credentialdiggerGetDiscoveries(&config, telemetryData, utils)
   103  	if err != nil {
   104  		// The exit number is the number of discoveries
   105  		// Therefore, this error is not relevant, if raised
   106  		log.Entry().Warn("There are findings to review")
   107  	}
   108  
   109  	// 4: Export report in workspace
   110  	reports := []piperutils.Path{}
   111  	reports = append(reports, piperutils.Path{Target: fmt.Sprintf("%v", piperReportName)})
   112  	piperutils.PersistReportsAndLinks("credentialdiggerScan", "./", utils, reports, nil)
   113  
   114  	return nil
   115  }
   116  
   117  func executeCredentialDiggerProcess(utils credentialdiggerUtils, args []string) error {
   118  	return utils.RunExecutable("credentialdigger", args...)
   119  }
   120  
   121  // hasConfigurationFile checks if the given file exists
   122  func hasRulesFile(file string, utils credentialdiggerUtils) bool {
   123  	exists, err := utils.FileExists(file)
   124  	if err != nil {
   125  		log.Entry().WithError(err).Error()
   126  	}
   127  	return exists
   128  }
   129  
   130  func credentialdiggerAddRules(config *credentialdiggerScanOptions, telemetryData *telemetry.CustomData, service credentialdiggerUtils) error {
   131  	// Credentialdigger home can be changed with local forks (e.g., for local piper runs)
   132  	cdHome := "/credential-digger-ui" // cdHome path as in docker container
   133  	if cdh := os.Getenv("CREDENTIALDIGGER_HOME"); cdh != "" {
   134  		cdHome = cdh
   135  	}
   136  	log.Entry().Debug("Use credentialdigger home ", cdHome)
   137  	// Set the rule file to the standard ruleset shipped within credential
   138  	// digger container
   139  	ruleFile := filepath.Join(cdHome, "backend", "rules.yml")
   140  
   141  	if config.RulesDownloadURL != "" {
   142  		// Download custom rule file from this URL
   143  		log.Entry().Debugf("Download custom ruleset from %v", config.RulesDownloadURL)
   144  		dlClient := piperhttp.Client{}
   145  		ruleFile := filepath.Join(cdHome, "backend", "custom-rules.yml")
   146  		dlClient.DownloadFile(config.RulesDownloadURL, ruleFile, nil, nil)
   147  		log.Entry().Info("Download and use remote rules")
   148  	} else {
   149  		log.Entry().Debug("Use a local ruleset")
   150  		// Use rules defined in stashed file
   151  		if hasRulesFile(config.RulesFile, service) {
   152  			log.Entry().WithField("file", config.RulesFile).Info("Use stashed rules file from repository")
   153  			ruleFile = config.RulesFile
   154  		} else {
   155  			log.Entry().Info("Use standard pre-defined rules")
   156  		}
   157  
   158  	}
   159  	cmd_list := []string{"add_rules", "--sqlite", piperDbName, ruleFile}
   160  	return executeCredentialDiggerProcess(service, cmd_list)
   161  }
   162  
   163  func credentialdiggerGetDiscoveries(config *credentialdiggerScanOptions, telemetryData *telemetry.CustomData, service credentialdiggerUtils) error {
   164  	log.Entry().Info("Get discoveries")
   165  	cmd_list := []string{"get_discoveries", config.Repository, "--sqlite", piperDbName,
   166  		"--save", piperReportName}
   167  	// Export all the discoveries or export only new ones
   168  	if !config.ExportAll {
   169  		cmd_list = append(cmd_list, "--state", "new")
   170  	}
   171  	err := executeCredentialDiggerProcess(service, cmd_list)
   172  	if err != nil {
   173  		log.Entry().Error("credentialdiggerScan: Failed running credentialdigger get_discoveries")
   174  		log.Entry().Error(err)
   175  		return err
   176  	}
   177  	log.Entry().Info("Scan complete")
   178  	return nil
   179  }
   180  
   181  func credentialdiggerBuildCommonArgs(config *credentialdiggerScanOptions) []string {
   182  	/*Some arguments are the same for all the scan flavors. Build them here
   183  	* not to duplicate code.*/
   184  	scan_args := []string{}
   185  	// Repository url and sqlite db (always mandatory)
   186  	scan_args = append(scan_args, config.Repository, "--sqlite", piperDbName)
   187  	//git token is not mandatory for base credential digger tool, but in
   188  	//piper it is
   189  	scan_args = append(scan_args, "--git_token", config.Token)
   190  	//debug
   191  	if config.Debug {
   192  		log.Entry().Debug("Run the scan in debug mode")
   193  		scan_args = append(scan_args, "--debug")
   194  	}
   195  	//models
   196  	if len(config.Models) > 0 {
   197  		log.Entry().Debugf("Enable models %v", config.Models)
   198  		scan_args = append(scan_args, "--models")
   199  		scan_args = append(scan_args, config.Models...)
   200  	}
   201  
   202  	return scan_args
   203  }
   204  
   205  func credentialdiggerScanSnapshot(config *credentialdiggerScanOptions, telemetryData *telemetry.CustomData, service credentialdiggerUtils) error {
   206  	log.Entry().Infof("Scan Snapshot %v from repo %v", config.Snapshot, config.Repository)
   207  	cmd_list := []string{"scan_snapshot",
   208  		"--snapshot", config.Snapshot}
   209  	cmd_list = append(cmd_list, credentialdiggerBuildCommonArgs(config)...)
   210  	leaks := executeCredentialDiggerProcess(service, cmd_list)
   211  	if leaks != nil {
   212  		log.Entry().Warn("The scan found potential leaks in this Snapshot")
   213  		return leaks
   214  	} else {
   215  		log.Entry().Info("No leaks found")
   216  		return nil
   217  	}
   218  }
   219  
   220  func credentialdiggerScanPR(config *credentialdiggerScanOptions, telemetryData *telemetry.CustomData, service credentialdiggerUtils) error {
   221  	log.Entry().Infof("Scan PR %v from repo %v", config.PrNumber, config.Repository)
   222  	cmd_list := []string{"scan_pr",
   223  		"--pr", strconv.Itoa(config.PrNumber),
   224  		"--api_endpoint", config.APIURL}
   225  	cmd_list = append(cmd_list, credentialdiggerBuildCommonArgs(config)...)
   226  	leaks := executeCredentialDiggerProcess(service, cmd_list)
   227  	if leaks != nil {
   228  		log.Entry().Warn("The scan found potential leaks in this PR")
   229  		return leaks
   230  	} else {
   231  		log.Entry().Info("No leaks found")
   232  		return nil
   233  	}
   234  }
   235  
   236  func credentialdiggerFullScan(config *credentialdiggerScanOptions, telemetryData *telemetry.CustomData, service credentialdiggerUtils) error {
   237  	log.Entry().Infof("Full scan of repository %v", config.Repository)
   238  	cmd_list := []string{"scan"}
   239  	cmd_list = append(cmd_list, credentialdiggerBuildCommonArgs(config)...)
   240  	leaks := executeCredentialDiggerProcess(service, cmd_list)
   241  	if leaks != nil {
   242  		log.Entry().Warn("The scan found potential leaks")
   243  		log.Entry().Warnf("%v potential leaks found", leaks)
   244  		return leaks
   245  	} else {
   246  		log.Entry().Info("No leaks found")
   247  		return nil
   248  	}
   249  }