github.com/jaylevin/jenkins-library@v1.230.4/cmd/fortifyExecuteScan.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"runtime"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    18  
    19  	"github.com/bmatcuk/doublestar"
    20  
    21  	"github.com/google/go-github/v32/github"
    22  	"github.com/google/uuid"
    23  
    24  	"github.com/piper-validation/fortify-client-go/models"
    25  
    26  	"github.com/SAP/jenkins-library/pkg/command"
    27  	"github.com/SAP/jenkins-library/pkg/fortify"
    28  	"github.com/SAP/jenkins-library/pkg/gradle"
    29  	"github.com/SAP/jenkins-library/pkg/log"
    30  	"github.com/SAP/jenkins-library/pkg/maven"
    31  	"github.com/SAP/jenkins-library/pkg/piperutils"
    32  	"github.com/SAP/jenkins-library/pkg/reporting"
    33  	"github.com/SAP/jenkins-library/pkg/telemetry"
    34  	"github.com/SAP/jenkins-library/pkg/toolrecord"
    35  	"github.com/SAP/jenkins-library/pkg/versioning"
    36  
    37  	piperGithub "github.com/SAP/jenkins-library/pkg/github"
    38  
    39  	"github.com/pkg/errors"
    40  )
    41  
    42  const getClasspathScriptContent = `
    43  gradle.allprojects {
    44      task getClasspath {
    45          doLast {
    46              new File(projectDir, filename).text = sourceSets.main.compileClasspath.asPath
    47          }
    48      }
    49  }
    50  `
    51  
    52  type pullRequestService interface {
    53  	ListPullRequestsWithCommit(ctx context.Context, owner, repo, sha string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error)
    54  }
    55  
    56  type fortifyUtils interface {
    57  	maven.Utils
    58  	gradle.Utils
    59  
    60  	SetDir(d string)
    61  	GetArtifact(buildTool, buildDescriptorFile string, options *versioning.Options) (versioning.Artifact, error)
    62  	CreateIssue(ghCreateIssueOptions *piperGithub.CreateIssueOptions) error
    63  }
    64  
    65  type fortifyUtilsBundle struct {
    66  	*command.Command
    67  	*piperutils.Files
    68  	*piperhttp.Client
    69  }
    70  
    71  func (f *fortifyUtilsBundle) GetArtifact(buildTool, buildDescriptorFile string, options *versioning.Options) (versioning.Artifact, error) {
    72  	return versioning.GetArtifact(buildTool, buildDescriptorFile, options, f)
    73  }
    74  
    75  func (f *fortifyUtilsBundle) CreateIssue(ghCreateIssueOptions *piperGithub.CreateIssueOptions) error {
    76  	return piperGithub.CreateIssue(ghCreateIssueOptions)
    77  }
    78  
    79  func newFortifyUtilsBundle() fortifyUtils {
    80  	utils := fortifyUtilsBundle{
    81  		Command: &command.Command{},
    82  		Files:   &piperutils.Files{},
    83  		Client:  &piperhttp.Client{},
    84  	}
    85  	utils.Stdout(log.Writer())
    86  	utils.Stderr(log.Writer())
    87  	return &utils
    88  }
    89  
    90  const checkString = "<---CHECK FORTIFY---"
    91  const classpathFileName = "fortify-execute-scan-cp.txt"
    92  
    93  func fortifyExecuteScan(config fortifyExecuteScanOptions, telemetryData *telemetry.CustomData, influx *fortifyExecuteScanInflux) {
    94  	auditStatus := map[string]string{}
    95  	sys := fortify.NewSystemInstance(config.ServerURL, config.APIEndpoint, config.AuthToken, time.Minute*15)
    96  	utils := newFortifyUtilsBundle()
    97  
    98  	influx.step_data.fields.fortify = false
    99  	reports, err := runFortifyScan(config, sys, utils, telemetryData, influx, auditStatus)
   100  	piperutils.PersistReportsAndLinks("fortifyExecuteScan", config.ModulePath, reports, nil)
   101  	if err != nil {
   102  		log.Entry().WithError(err).Fatal("Fortify scan and check failed")
   103  	}
   104  	influx.step_data.fields.fortify = true
   105  	// make sure that no specific error category is set in success case
   106  	log.SetErrorCategory(log.ErrorUndefined)
   107  }
   108  
   109  func determineArtifact(config fortifyExecuteScanOptions, utils fortifyUtils) (versioning.Artifact, error) {
   110  	versioningOptions := versioning.Options{
   111  		M2Path:              config.M2Path,
   112  		GlobalSettingsFile:  config.GlobalSettingsFile,
   113  		ProjectSettingsFile: config.ProjectSettingsFile,
   114  	}
   115  
   116  	artifact, err := utils.GetArtifact(config.BuildTool, config.BuildDescriptorFile, &versioningOptions)
   117  	if err != nil {
   118  		return nil, fmt.Errorf("Unable to get artifact from descriptor %v: %w", config.BuildDescriptorFile, err)
   119  	}
   120  	return artifact, nil
   121  }
   122  
   123  func runFortifyScan(config fortifyExecuteScanOptions, sys fortify.System, utils fortifyUtils, telemetryData *telemetry.CustomData, influx *fortifyExecuteScanInflux, auditStatus map[string]string) ([]piperutils.Path, error) {
   124  	var reports []piperutils.Path
   125  	log.Entry().Debugf("Running Fortify scan against SSC at %v", config.ServerURL)
   126  
   127  	if config.BuildTool == "maven" && config.InstallArtifacts {
   128  		err := maven.InstallMavenArtifacts(&maven.EvaluateOptions{
   129  			M2Path:              config.M2Path,
   130  			ProjectSettingsFile: config.ProjectSettingsFile,
   131  			GlobalSettingsFile:  config.GlobalSettingsFile,
   132  			PomPath:             config.BuildDescriptorFile,
   133  		}, utils)
   134  		if err != nil {
   135  			return reports, fmt.Errorf("Unable to install artifacts: %w", err)
   136  		}
   137  	}
   138  
   139  	artifact, err := determineArtifact(config, utils)
   140  	if err != nil {
   141  		log.Entry().WithError(err).Fatal()
   142  	}
   143  	coordinates, err := artifact.GetCoordinates()
   144  	if err != nil {
   145  		log.SetErrorCategory(log.ErrorConfiguration)
   146  		return reports, fmt.Errorf("unable to get project coordinates from descriptor %v: %w", config.BuildDescriptorFile, err)
   147  	}
   148  	log.Entry().Debugf("loaded project coordinates %v from descriptor", coordinates)
   149  
   150  	if len(config.Version) > 0 {
   151  		log.Entry().Infof("Resolving product version from default provided '%s' with versioning '%s'", config.Version, config.VersioningModel)
   152  		coordinates.Version = config.Version
   153  	}
   154  
   155  	fortifyProjectName, fortifyProjectVersion := versioning.DetermineProjectCoordinatesWithCustomVersion(config.ProjectName, config.VersioningModel, config.CustomScanVersion, coordinates)
   156  	project, err := sys.GetProjectByName(fortifyProjectName, config.AutoCreate, fortifyProjectVersion)
   157  	if err != nil {
   158  		classifyErrorOnLookup(err)
   159  		return reports, fmt.Errorf("Failed to load project %v: %w", fortifyProjectName, err)
   160  	}
   161  	projectVersion, err := sys.GetProjectVersionDetailsByProjectIDAndVersionName(project.ID, fortifyProjectVersion, config.AutoCreate, fortifyProjectName)
   162  	if err != nil {
   163  		classifyErrorOnLookup(err)
   164  		return reports, fmt.Errorf("Failed to load project version %v: %w", fortifyProjectVersion, err)
   165  	}
   166  
   167  	if len(config.PullRequestName) > 0 {
   168  		fortifyProjectVersion = config.PullRequestName
   169  		projectVersion, err = sys.LookupOrCreateProjectVersionDetailsForPullRequest(project.ID, projectVersion, fortifyProjectVersion)
   170  		if err != nil {
   171  			classifyErrorOnLookup(err)
   172  			return reports, fmt.Errorf("Failed to lookup / create project version for pull request %v: %w", fortifyProjectVersion, err)
   173  		}
   174  		log.Entry().Debugf("Looked up / created project version with ID %v for PR %v", projectVersion.ID, fortifyProjectVersion)
   175  	} else {
   176  		prID, prAuthor := determinePullRequestMerge(config)
   177  		if prID != "0" {
   178  			log.Entry().Debugf("Determined PR ID '%v' for merge check", prID)
   179  			if len(prAuthor) > 0 && !piperutils.ContainsString(config.Assignees, prAuthor) {
   180  				log.Entry().Debugf("Determined PR Author '%v' for result assignment", prAuthor)
   181  				config.Assignees = append(config.Assignees, prAuthor)
   182  			} else {
   183  				log.Entry().Debugf("Unable to determine PR Author, using assignees: %v", config.Assignees)
   184  			}
   185  			pullRequestProjectName := fmt.Sprintf("PR-%v", prID)
   186  			err = sys.MergeProjectVersionStateOfPRIntoMaster(config.FprDownloadEndpoint, config.FprUploadEndpoint, project.ID, projectVersion.ID, pullRequestProjectName)
   187  			if err != nil {
   188  				return reports, fmt.Errorf("Failed to merge project version state for pull request %v into project version %v of project %v: %w", pullRequestProjectName, fortifyProjectVersion, project.ID, err)
   189  			}
   190  		}
   191  	}
   192  
   193  	filterSet, err := sys.GetFilterSetOfProjectVersionByTitle(projectVersion.ID, config.FilterSetTitle)
   194  	if filterSet == nil || err != nil {
   195  		return reports, fmt.Errorf("Failed to load filter set with title %v", config.FilterSetTitle)
   196  	}
   197  
   198  	// create toolrecord file
   199  	// tbd - how to handle verifyOnly
   200  	toolRecordFileName, err := createToolRecordFortify("./", config, project.ID, fortifyProjectName, projectVersion.ID, fortifyProjectVersion)
   201  	if err != nil {
   202  		// do not fail until the framework is well established
   203  		log.Entry().Warning("TR_FORTIFY: Failed to create toolrecord file ...", err)
   204  	} else {
   205  		reports = append(reports, piperutils.Path{Target: toolRecordFileName})
   206  	}
   207  
   208  	if config.VerifyOnly {
   209  		log.Entry().Infof("Starting audit status check on project %v with version %v and project version ID %v", fortifyProjectName, fortifyProjectVersion, projectVersion.ID)
   210  		err, paths := verifyFFProjectCompliance(config, utils, sys, project, projectVersion, filterSet, influx, auditStatus)
   211  		reports = append(reports, paths...)
   212  		return reports, err
   213  	}
   214  
   215  	log.Entry().Infof("Scanning and uploading to project %v with version %v and projectVersionId %v", fortifyProjectName, fortifyProjectVersion, projectVersion.ID)
   216  	buildLabel := fmt.Sprintf("%v/repos/%v/%v/commits/%v", config.GithubAPIURL, config.Owner, config.Repository, config.CommitID)
   217  
   218  	// Create sourceanalyzer command based on configuration
   219  	buildID := uuid.New().String()
   220  	utils.SetDir(config.ModulePath)
   221  	os.MkdirAll(fmt.Sprintf("%v/%v", config.ModulePath, "target"), os.ModePerm)
   222  
   223  	if config.UpdateRulePack {
   224  		err := utils.RunExecutable("fortifyupdate", "-acceptKey", "-acceptSSLCertificate", "-url", config.ServerURL)
   225  		if err != nil {
   226  			return reports, fmt.Errorf("failed to update rule pack, serverUrl: %v", config.ServerURL)
   227  		}
   228  		err = utils.RunExecutable("fortifyupdate", "-acceptKey", "-acceptSSLCertificate", "-showInstalledRules")
   229  		if err != nil {
   230  			return reports, fmt.Errorf("failed to fetch details of installed rule pack, serverUrl: %v", config.ServerURL)
   231  		}
   232  	}
   233  
   234  	err = triggerFortifyScan(config, utils, buildID, buildLabel, fortifyProjectName)
   235  	reports = append(reports, piperutils.Path{Target: fmt.Sprintf("%vtarget/fortify-scan.*", config.ModulePath)})
   236  	reports = append(reports, piperutils.Path{Target: fmt.Sprintf("%vtarget/*.fpr", config.ModulePath)})
   237  	if err != nil {
   238  		return reports, errors.Wrapf(err, "failed to scan project")
   239  	}
   240  
   241  	var message string
   242  	if config.UploadResults {
   243  		log.Entry().Debug("Uploading results")
   244  		resultFilePath := fmt.Sprintf("%vtarget/result.fpr", config.ModulePath)
   245  		err = sys.UploadResultFile(config.FprUploadEndpoint, resultFilePath, projectVersion.ID)
   246  		message = fmt.Sprintf("Failed to upload result file %v to Fortify SSC at %v", resultFilePath, config.ServerURL)
   247  	} else {
   248  		log.Entry().Debug("Generating XML report")
   249  		xmlReportName := "fortify_result.xml"
   250  		err = utils.RunExecutable("ReportGenerator", "-format", "xml", "-f", xmlReportName, "-source", fmt.Sprintf("%vtarget/result.fpr", config.ModulePath))
   251  		message = fmt.Sprintf("Failed to generate XML report %v", xmlReportName)
   252  		if err != nil {
   253  			reports = append(reports, piperutils.Path{Target: fmt.Sprintf("%vfortify_result.xml", config.ModulePath)})
   254  		}
   255  	}
   256  	if err != nil {
   257  		return reports, fmt.Errorf(message+": %w", err)
   258  	}
   259  
   260  	log.Entry().Infof("Ensuring latest FPR is processed for project %v with version %v and project version ID %v", fortifyProjectName, fortifyProjectVersion, projectVersion.ID)
   261  	// Ensure latest FPR is processed
   262  	err = verifyScanResultsFinishedUploading(config, sys, projectVersion.ID, buildLabel, filterSet,
   263  		10*time.Second, time.Duration(config.PollingMinutes)*time.Minute)
   264  	if err != nil {
   265  		return reports, err
   266  	}
   267  
   268  	// SARIF conversion done after latest FPR is processed, but before the compliance is checked
   269  	if config.ConvertToSarif {
   270  		resultFilePath := fmt.Sprintf("%vtarget/result.fpr", config.ModulePath)
   271  		log.Entry().Info("Calling conversion to SARIF function.")
   272  		sarif, err := fortify.ConvertFprToSarif(sys, project, projectVersion, resultFilePath, filterSet)
   273  		if err != nil {
   274  			return reports, fmt.Errorf("failed to generate SARIF")
   275  		}
   276  		log.Entry().Debug("Writing sarif file to disk.")
   277  		paths, err := fortify.WriteSarif(sarif)
   278  		if err != nil {
   279  			return reports, fmt.Errorf("failed to write sarif")
   280  		}
   281  		reports = append(reports, paths...)
   282  	}
   283  
   284  	log.Entry().Infof("Starting audit status check on project %v with version %v and project version ID %v", fortifyProjectName, fortifyProjectVersion, projectVersion.ID)
   285  	err, paths := verifyFFProjectCompliance(config, utils, sys, project, projectVersion, filterSet, influx, auditStatus)
   286  	reports = append(reports, paths...)
   287  	return reports, err
   288  }
   289  
   290  func classifyErrorOnLookup(err error) {
   291  	if strings.Contains(err.Error(), "connect: connection refused") || strings.Contains(err.Error(), "net/http: TLS handshake timeout") {
   292  		log.SetErrorCategory(log.ErrorService)
   293  	}
   294  }
   295  
   296  func verifyFFProjectCompliance(config fortifyExecuteScanOptions, utils fortifyUtils, sys fortify.System, project *models.Project, projectVersion *models.ProjectVersion, filterSet *models.FilterSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string) (error, []piperutils.Path) {
   297  	reports := []piperutils.Path{}
   298  	// Generate report
   299  	if config.Reporting {
   300  		resultURL := []byte(fmt.Sprintf("%v/html/ssc/version/%v/fix/null/", config.ServerURL, projectVersion.ID))
   301  		ioutil.WriteFile(fmt.Sprintf("%vtarget/%v-%v.%v", config.ModulePath, *project.Name, *projectVersion.Name, "txt"), resultURL, 0700)
   302  
   303  		data, err := generateAndDownloadQGateReport(config, sys, project, projectVersion)
   304  		if err != nil {
   305  			return err, reports
   306  		}
   307  		ioutil.WriteFile(fmt.Sprintf("%vtarget/%v-%v.%v", config.ModulePath, *project.Name, *projectVersion.Name, config.ReportType), data, 0700)
   308  	}
   309  
   310  	// Perform audit compliance checks
   311  	issueFilterSelectorSet, err := sys.GetIssueFilterSelectorOfProjectVersionByName(projectVersion.ID, []string{"Analysis", "Folder", "Category"}, nil)
   312  	if err != nil {
   313  		return errors.Wrapf(err, "failed to fetch project version issue filter selector for project version ID %v", projectVersion.ID), reports
   314  	}
   315  	log.Entry().Debugf("initial filter selector set: %v", issueFilterSelectorSet)
   316  
   317  	spotChecksCountByCategory := []fortify.SpotChecksAuditCount{}
   318  	numberOfViolations, issueGroups, err := analyseUnauditedIssues(config, sys, projectVersion, filterSet, issueFilterSelectorSet, influx, auditStatus, &spotChecksCountByCategory)
   319  	if err != nil {
   320  		return errors.Wrap(err, "failed to analyze unaudited issues"), reports
   321  	}
   322  	numberOfSuspiciousExploitable, issueGroupsSuspiciousExploitable := analyseSuspiciousExploitable(config, sys, projectVersion, filterSet, issueFilterSelectorSet, influx, auditStatus)
   323  	numberOfViolations += numberOfSuspiciousExploitable
   324  	issueGroups = append(issueGroups, issueGroupsSuspiciousExploitable...)
   325  
   326  	log.Entry().Infof("Counted %v violations, details: %v", numberOfViolations, auditStatus)
   327  
   328  	influx.fortify_data.fields.projectID = project.ID
   329  	influx.fortify_data.fields.projectName = *project.Name
   330  	influx.fortify_data.fields.projectVersion = *projectVersion.Name
   331  	influx.fortify_data.fields.projectVersionID = projectVersion.ID
   332  	influx.fortify_data.fields.violations = numberOfViolations
   333  
   334  	fortifyReportingData := prepareReportData(influx)
   335  	scanReport := fortify.CreateCustomReport(fortifyReportingData, issueGroups)
   336  	paths, err := fortify.WriteCustomReports(scanReport)
   337  	if err != nil {
   338  		return errors.Wrap(err, "failed to write custom reports"), reports
   339  	}
   340  	reports = append(reports, paths...)
   341  
   342  	log.Entry().Debug("Checking whether GitHub issue creation/update is active")
   343  	log.Entry().Debugf("%v, %v, %v, %v, %v, %v", config.CreateResultIssue, numberOfViolations > 0, len(config.GithubToken) > 0, len(config.GithubAPIURL) > 0, len(config.Owner) > 0, len(config.Repository) > 0)
   344  	if config.CreateResultIssue && numberOfViolations > 0 && len(config.GithubToken) > 0 && len(config.GithubAPIURL) > 0 && len(config.Owner) > 0 && len(config.Repository) > 0 {
   345  		log.Entry().Debug("Creating/updating GitHub issue with scan results")
   346  		err = reporting.UploadSingleReportToGithub(scanReport, config.GithubToken, config.GithubAPIURL, config.Owner, config.Repository, config.Assignees, utils)
   347  		if err != nil {
   348  			return errors.Wrap(err, "failed to upload scan results into GitHub"), reports
   349  		}
   350  	}
   351  
   352  	jsonReport := fortify.CreateJSONReport(fortifyReportingData, spotChecksCountByCategory, config.ServerURL)
   353  	paths, err = fortify.WriteJSONReport(jsonReport)
   354  	if err != nil {
   355  		return errors.Wrap(err, "failed to write json report"), reports
   356  	}
   357  	reports = append(reports, paths...)
   358  
   359  	if numberOfViolations > 0 {
   360  		log.SetErrorCategory(log.ErrorCompliance)
   361  		return errors.New("fortify scan failed, the project is not compliant. For details check the archived report"), reports
   362  	}
   363  	return nil, reports
   364  }
   365  
   366  func prepareReportData(influx *fortifyExecuteScanInflux) fortify.FortifyReportData {
   367  	input := influx.fortify_data.fields
   368  	output := fortify.FortifyReportData{}
   369  	output.ProjectID = input.projectID
   370  	output.ProjectName = input.projectName
   371  	output.ProjectVersion = input.projectVersion
   372  	output.AuditAllAudited = input.auditAllAudited
   373  	output.AuditAllTotal = input.auditAllTotal
   374  	output.CorporateAudited = input.corporateAudited
   375  	output.CorporateTotal = input.corporateTotal
   376  	output.SpotChecksAudited = input.spotChecksAudited
   377  	output.SpotChecksGap = input.spotChecksGap
   378  	output.SpotChecksTotal = input.spotChecksTotal
   379  	output.Exploitable = input.exploitable
   380  	output.Suppressed = input.suppressed
   381  	output.Suspicious = input.suspicious
   382  	output.ProjectVersionID = input.projectVersionID
   383  	output.Violations = input.violations
   384  	return output
   385  }
   386  
   387  func analyseUnauditedIssues(config fortifyExecuteScanOptions, sys fortify.System, projectVersion *models.ProjectVersion, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string, spotChecksCountByCategory *[]fortify.SpotChecksAuditCount) (int, []*models.ProjectVersionIssueGroup, error) {
   388  	log.Entry().Info("Analyzing unaudited issues")
   389  	reducedFilterSelectorSet := sys.ReduceIssueFilterSelectorSet(issueFilterSelectorSet, []string{"Folder"}, nil)
   390  	fetchedIssueGroups, err := sys.GetProjectIssuesByIDAndFilterSetGroupedBySelector(projectVersion.ID, "", filterSet.GUID, reducedFilterSelectorSet)
   391  	if err != nil {
   392  		return 0, fetchedIssueGroups, errors.Wrapf(err, "failed to fetch project version issue groups with filter set %v and selector %v for project version ID %v", filterSet, issueFilterSelectorSet, projectVersion.ID)
   393  	}
   394  	overallViolations := 0
   395  	for _, issueGroup := range fetchedIssueGroups {
   396  		issueDelta, err := getIssueDeltaFor(config, sys, issueGroup, projectVersion.ID, filterSet, issueFilterSelectorSet, influx, auditStatus, spotChecksCountByCategory)
   397  		if err != nil {
   398  			return overallViolations, fetchedIssueGroups, errors.Wrap(err, "failed to get issue delta")
   399  		}
   400  		overallViolations += issueDelta
   401  	}
   402  	return overallViolations, fetchedIssueGroups, nil
   403  }
   404  
   405  func getIssueDeltaFor(config fortifyExecuteScanOptions, sys fortify.System, issueGroup *models.ProjectVersionIssueGroup, projectVersionID int64, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string, spotChecksCountByCategory *[]fortify.SpotChecksAuditCount) (int, error) {
   406  	totalMinusAuditedDelta := 0
   407  	group := ""
   408  	total := 0
   409  	audited := 0
   410  	if issueGroup != nil {
   411  		group = *issueGroup.ID
   412  		total = int(*issueGroup.TotalCount)
   413  		audited = int(*issueGroup.AuditedCount)
   414  	}
   415  	groupTotalMinusAuditedDelta := total - audited
   416  	if groupTotalMinusAuditedDelta > 0 {
   417  		reducedFilterSelectorSet := sys.ReduceIssueFilterSelectorSet(issueFilterSelectorSet, []string{"Folder", "Analysis"}, []string{group})
   418  		folderSelector := sys.GetFilterSetByDisplayName(reducedFilterSelectorSet, "Folder")
   419  		if folderSelector == nil {
   420  			return totalMinusAuditedDelta, fmt.Errorf("folder selector not found")
   421  		}
   422  		analysisSelector := sys.GetFilterSetByDisplayName(reducedFilterSelectorSet, "Analysis")
   423  
   424  		auditStatus[group] = fmt.Sprintf("%v total : %v audited", total, audited)
   425  
   426  		if strings.Contains(config.MustAuditIssueGroups, group) {
   427  			totalMinusAuditedDelta += groupTotalMinusAuditedDelta
   428  			if group == "Corporate Security Requirements" {
   429  				influx.fortify_data.fields.corporateTotal = total
   430  				influx.fortify_data.fields.corporateAudited = audited
   431  			}
   432  			if group == "Audit All" {
   433  				influx.fortify_data.fields.auditAllTotal = total
   434  				influx.fortify_data.fields.auditAllAudited = audited
   435  			}
   436  			log.Entry().Errorf("[projectVersionId %v]: Unaudited %v detected, count %v", projectVersionID, group, totalMinusAuditedDelta)
   437  			logIssueURL(config, projectVersionID, folderSelector, analysisSelector)
   438  		}
   439  
   440  		if strings.Contains(config.SpotAuditIssueGroups, group) {
   441  			log.Entry().Infof("Analyzing %v", config.SpotAuditIssueGroups)
   442  			filter := fmt.Sprintf("%v:%v", folderSelector.EntityType, folderSelector.SelectorOptions[0].Value)
   443  			fetchedIssueGroups, err := sys.GetProjectIssuesByIDAndFilterSetGroupedBySelector(projectVersionID, filter, filterSet.GUID, sys.ReduceIssueFilterSelectorSet(issueFilterSelectorSet, []string{"Category"}, nil))
   444  			if err != nil {
   445  				return totalMinusAuditedDelta, errors.Wrapf(err, "failed to fetch project version issue groups with filter %v, filter set %v and selector %v for project version ID %v", filter, filterSet, issueFilterSelectorSet, projectVersionID)
   446  			}
   447  			totalMinusAuditedDelta += getSpotIssueCount(config, sys, fetchedIssueGroups, projectVersionID, filterSet, reducedFilterSelectorSet, influx, auditStatus, spotChecksCountByCategory)
   448  		}
   449  	}
   450  	return totalMinusAuditedDelta, nil
   451  }
   452  
   453  func getSpotIssueCount(config fortifyExecuteScanOptions, sys fortify.System, spotCheckCategories []*models.ProjectVersionIssueGroup, projectVersionID int64, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string, spotChecksCountByCategory *[]fortify.SpotChecksAuditCount) int {
   454  	overallDelta := 0
   455  	overallIssues := 0
   456  	overallIssuesAudited := 0
   457  	for _, issueGroup := range spotCheckCategories {
   458  		group := ""
   459  		total := 0
   460  		audited := 0
   461  		if issueGroup != nil {
   462  			group = *issueGroup.ID
   463  			total = int(*issueGroup.TotalCount)
   464  			audited = int(*issueGroup.AuditedCount)
   465  		}
   466  		flagOutput := ""
   467  
   468  		if ((total <= config.SpotCheckMinimum || config.SpotCheckMinimum < 0) && audited != total) || (total > config.SpotCheckMinimum && audited < config.SpotCheckMinimum) {
   469  			currentDelta := config.SpotCheckMinimum - audited
   470  			if config.SpotCheckMinimum < 0 || config.SpotCheckMinimum > total {
   471  				currentDelta = total - audited
   472  			}
   473  			if currentDelta > 0 {
   474  				filterSelectorFolder := sys.GetFilterSetByDisplayName(issueFilterSelectorSet, "Folder")
   475  				filterSelectorAnalysis := sys.GetFilterSetByDisplayName(issueFilterSelectorSet, "Analysis")
   476  				overallDelta += currentDelta
   477  				log.Entry().Errorf("[projectVersionId %v]: %v unaudited spot check issues detected in group %v", projectVersionID, currentDelta, group)
   478  				logIssueURL(config, projectVersionID, filterSelectorFolder, filterSelectorAnalysis)
   479  				flagOutput = checkString
   480  			}
   481  		}
   482  
   483  		overallIssues += total
   484  		overallIssuesAudited += audited
   485  
   486  		auditStatus[group] = fmt.Sprintf("%v total : %v audited %v", total, audited, flagOutput)
   487  		*spotChecksCountByCategory = append(*spotChecksCountByCategory, fortify.SpotChecksAuditCount{Audited: audited, Total: total, Type: group})
   488  	}
   489  
   490  	influx.fortify_data.fields.spotChecksTotal = overallIssues
   491  	influx.fortify_data.fields.spotChecksAudited = overallIssuesAudited
   492  	influx.fortify_data.fields.spotChecksGap = overallDelta
   493  
   494  	return overallDelta
   495  }
   496  
   497  func analyseSuspiciousExploitable(config fortifyExecuteScanOptions, sys fortify.System, projectVersion *models.ProjectVersion, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string) (int, []*models.ProjectVersionIssueGroup) {
   498  	log.Entry().Info("Analyzing suspicious and exploitable issues")
   499  	reducedFilterSelectorSet := sys.ReduceIssueFilterSelectorSet(issueFilterSelectorSet, []string{"Analysis"}, []string{})
   500  	fetchedGroups, err := sys.GetProjectIssuesByIDAndFilterSetGroupedBySelector(projectVersion.ID, "", filterSet.GUID, reducedFilterSelectorSet)
   501  
   502  	suspiciousCount := 0
   503  	exploitableCount := 0
   504  	for _, issueGroup := range fetchedGroups {
   505  		if *issueGroup.ID == "3" {
   506  			suspiciousCount = int(*issueGroup.TotalCount)
   507  		} else if *issueGroup.ID == "4" {
   508  			exploitableCount = int(*issueGroup.TotalCount)
   509  		}
   510  	}
   511  
   512  	result := 0
   513  	if (suspiciousCount > 0 && config.ConsiderSuspicious) || exploitableCount > 0 {
   514  		result = result + suspiciousCount + exploitableCount
   515  		log.Entry().Errorf("[projectVersionId %v]: %v suspicious and %v exploitable issues detected", projectVersion.ID, suspiciousCount, exploitableCount)
   516  		log.Entry().Errorf("%v/html/ssc/index.jsp#!/version/%v/fix?issueGrouping=%v_%v&issueFilters=%v_%v", config.ServerURL, projectVersion.ID, reducedFilterSelectorSet.GroupBySet[0].EntityType, reducedFilterSelectorSet.GroupBySet[0].Value, reducedFilterSelectorSet.FilterBySet[0].EntityType, reducedFilterSelectorSet.FilterBySet[0].Value)
   517  	}
   518  	issueStatistics, err := sys.GetIssueStatisticsOfProjectVersion(projectVersion.ID)
   519  	if err != nil {
   520  		log.Entry().WithError(err).Errorf("Failed to fetch project version statistics for project version ID %v", projectVersion.ID)
   521  	}
   522  	auditStatus["Suspicious"] = fmt.Sprintf("%v", suspiciousCount)
   523  	auditStatus["Exploitable"] = fmt.Sprintf("%v", exploitableCount)
   524  	suppressedCount := *issueStatistics[0].SuppressedCount
   525  	if suppressedCount > 0 {
   526  		auditStatus["Suppressed"] = fmt.Sprintf("WARNING: Detected %v suppressed issues which could violate audit compliance!!!", suppressedCount)
   527  	}
   528  	influx.fortify_data.fields.suspicious = suspiciousCount
   529  	influx.fortify_data.fields.exploitable = exploitableCount
   530  	influx.fortify_data.fields.suppressed = int(suppressedCount)
   531  
   532  	return result, fetchedGroups
   533  }
   534  
   535  func logIssueURL(config fortifyExecuteScanOptions, projectVersionID int64, folderSelector, analysisSelector *models.IssueFilterSelector) {
   536  	url := fmt.Sprintf("%v/html/ssc/index.jsp#!/version/%v/fix", config.ServerURL, projectVersionID)
   537  	if len(folderSelector.SelectorOptions) > 0 {
   538  		url += fmt.Sprintf("?issueFilters=%v_%v:%v",
   539  			folderSelector.EntityType,
   540  			folderSelector.Value,
   541  			folderSelector.SelectorOptions[0].Value)
   542  	} else {
   543  		log.Entry().Debugf("no 'filter by set' array entries")
   544  	}
   545  	if analysisSelector != nil {
   546  		url += fmt.Sprintf("&issueFilters=%v_%v:",
   547  			analysisSelector.EntityType,
   548  			analysisSelector.Value)
   549  	} else {
   550  		log.Entry().Debugf("no second entry in 'filter by set' array")
   551  	}
   552  	log.Entry().Error(url)
   553  }
   554  
   555  func generateAndDownloadQGateReport(config fortifyExecuteScanOptions, sys fortify.System, project *models.Project, projectVersion *models.ProjectVersion) ([]byte, error) {
   556  	log.Entry().Infof("Generating report with template ID %v", config.ReportTemplateID)
   557  	report, err := sys.GenerateQGateReport(project.ID, projectVersion.ID, int64(config.ReportTemplateID), *project.Name, *projectVersion.Name, config.ReportType)
   558  	if err != nil {
   559  		return []byte{}, errors.Wrap(err, "failed to generate Q-Gate report")
   560  	}
   561  	log.Entry().Debugf("Triggered report generation of report ID %v", report.ID)
   562  	status := report.Status
   563  	for status == "PROCESSING" || status == "SCHED_PROCESSING" {
   564  		time.Sleep(10 * time.Second)
   565  		report, err = sys.GetReportDetails(report.ID)
   566  		if err != nil {
   567  			return []byte{}, fmt.Errorf("Failed to fetch Q-Gate report generation status: %w", err)
   568  		}
   569  		status = report.Status
   570  	}
   571  	data, err := sys.DownloadReportFile(config.ReportDownloadEndpoint, report.ID)
   572  	if err != nil {
   573  		return []byte{}, fmt.Errorf("Failed to download Q-Gate Report: %w", err)
   574  	}
   575  	return data, nil
   576  }
   577  
   578  var errProcessing = errors.New("artifact still processing")
   579  
   580  func checkArtifactStatus(config fortifyExecuteScanOptions, projectVersionID int64, filterSet *models.FilterSet, artifact *models.Artifact, retries int, pollingDelay, timeout time.Duration) error {
   581  	if "PROCESSING" == artifact.Status || "SCHED_PROCESSING" == artifact.Status {
   582  		pollingTime := time.Duration(retries) * pollingDelay
   583  		if pollingTime >= timeout {
   584  			log.SetErrorCategory(log.ErrorService)
   585  			return fmt.Errorf("terminating after %v since artifact for Project Version %v is still in status %v", timeout, projectVersionID, artifact.Status)
   586  		}
   587  		log.Entry().Infof("Most recent artifact uploaded on %v of Project Version %v is still in status %v...", artifact.UploadDate, projectVersionID, artifact.Status)
   588  		time.Sleep(pollingDelay)
   589  		return errProcessing
   590  	}
   591  	if "REQUIRE_AUTH" == artifact.Status {
   592  		// verify no manual issue approval needed
   593  		log.SetErrorCategory(log.ErrorCompliance)
   594  		return fmt.Errorf("There are artifacts that require manual approval for Project Version %v, please visit Fortify SSC and approve them for processing\n%v/html/ssc/index.jsp#!/version/%v/artifacts?filterSet=%v", projectVersionID, config.ServerURL, projectVersionID, filterSet.GUID)
   595  	}
   596  	if "ERROR_PROCESSING" == artifact.Status {
   597  		log.SetErrorCategory(log.ErrorService)
   598  		return fmt.Errorf("There are artifacts that failed processing for Project Version %v\n%v/html/ssc/index.jsp#!/version/%v/artifacts?filterSet=%v", projectVersionID, config.ServerURL, projectVersionID, filterSet.GUID)
   599  	}
   600  	return nil
   601  }
   602  
   603  func verifyScanResultsFinishedUploading(config fortifyExecuteScanOptions, sys fortify.System, projectVersionID int64, buildLabel string, filterSet *models.FilterSet, pollingDelay, timeout time.Duration) error {
   604  	log.Entry().Debug("Verifying scan results have finished uploading and processing")
   605  	var artifacts []*models.Artifact
   606  	var relatedUpload *models.Artifact
   607  	var err error
   608  	retries := 0
   609  	for relatedUpload == nil {
   610  		artifacts, err = sys.GetArtifactsOfProjectVersion(projectVersionID)
   611  		log.Entry().Debugf("Received %v artifacts for project version ID %v", len(artifacts), projectVersionID)
   612  		if err != nil {
   613  			return fmt.Errorf("failed to fetch artifacts of project version ID %v", projectVersionID)
   614  		}
   615  		if len(artifacts) == 0 {
   616  			return fmt.Errorf("no uploaded artifacts for assessment detected for project version with ID %v", projectVersionID)
   617  		}
   618  		latest := artifacts[0]
   619  		err = checkArtifactStatus(config, projectVersionID, filterSet, latest, retries, pollingDelay, timeout)
   620  		if err != nil {
   621  			if err == errProcessing {
   622  				retries++
   623  				continue
   624  			}
   625  			return err
   626  		}
   627  		relatedUpload = findArtifactByBuildLabel(artifacts, buildLabel)
   628  		if relatedUpload == nil {
   629  			log.Entry().Warn("Unable to identify artifact based on the build label, will consider most recent artifact as related to the scan")
   630  			relatedUpload = artifacts[0]
   631  		}
   632  	}
   633  
   634  	differenceInSeconds := calculateTimeDifferenceToLastUpload(relatedUpload.UploadDate, projectVersionID)
   635  	// Use the absolute value for checking the time difference
   636  	if differenceInSeconds > float64(60*config.DeltaMinutes) {
   637  		return errors.New("no recent upload detected on Project Version")
   638  	}
   639  	for _, upload := range artifacts {
   640  		if upload.Status == "ERROR_PROCESSING" {
   641  			log.Entry().Warn("Previous uploads detected that failed processing, please ensure that your scans are properly configured")
   642  			break
   643  		}
   644  	}
   645  	return nil
   646  }
   647  
   648  func findArtifactByBuildLabel(artifacts []*models.Artifact, buildLabel string) *models.Artifact {
   649  	if len(buildLabel) == 0 {
   650  		return nil
   651  	}
   652  	for _, artifact := range artifacts {
   653  		if len(buildLabel) > 0 && artifact.Embed != nil && artifact.Embed.Scans != nil && len(artifact.Embed.Scans) > 0 {
   654  			scan := artifact.Embed.Scans[0]
   655  			if scan != nil && strings.HasSuffix(scan.BuildLabel, buildLabel) {
   656  				return artifact
   657  			}
   658  		}
   659  	}
   660  	return nil
   661  }
   662  
   663  func calculateTimeDifferenceToLastUpload(uploadDate models.Iso8601MilliDateTime, projectVersionID int64) float64 {
   664  	log.Entry().Infof("Last upload on project version %v happened on %v", projectVersionID, uploadDate)
   665  	uploadDateAsTime := time.Time(uploadDate)
   666  	duration := time.Since(uploadDateAsTime)
   667  	log.Entry().Debugf("Difference duration is %v", duration)
   668  	absoluteSeconds := math.Abs(duration.Seconds())
   669  	log.Entry().Infof("Difference since %v in seconds is %v", uploadDateAsTime, absoluteSeconds)
   670  	return absoluteSeconds
   671  }
   672  
   673  func executeTemplatedCommand(utils fortifyUtils, cmdTemplate []string, context map[string]string) error {
   674  	for index, cmdTemplatePart := range cmdTemplate {
   675  		result, err := piperutils.ExecuteTemplate(cmdTemplatePart, context)
   676  		if err != nil {
   677  			return errors.Wrapf(err, "failed to transform template for command fragment: %v", cmdTemplatePart)
   678  		}
   679  		cmdTemplate[index] = result
   680  	}
   681  	err := utils.RunExecutable(cmdTemplate[0], cmdTemplate[1:]...)
   682  	if err != nil {
   683  		return errors.Wrapf(err, "failed to execute command %v", cmdTemplate)
   684  	}
   685  	return nil
   686  }
   687  
   688  func autoresolvePipClasspath(executable string, parameters []string, file string, utils fortifyUtils) (string, error) {
   689  	// redirect stdout and create cp file from command output
   690  	outfile, err := os.Create(file)
   691  	if err != nil {
   692  		return "", errors.Wrapf(err, "failed to create classpath file")
   693  	}
   694  	defer outfile.Close()
   695  	utils.Stdout(outfile)
   696  	err = utils.RunExecutable(executable, parameters...)
   697  	if err != nil {
   698  		return "", errors.Wrapf(err, "failed to run classpath autodetection command %v with parameters %v", executable, parameters)
   699  	}
   700  	utils.Stdout(log.Entry().Writer())
   701  	return readClasspathFile(file), nil
   702  }
   703  
   704  func autoresolveMavenClasspath(config fortifyExecuteScanOptions, file string, utils fortifyUtils) (string, error) {
   705  	if filepath.IsAbs(file) {
   706  		log.Entry().Warnf("Passing an absolute path for -Dmdep.outputFile results in the classpath only for the last module in multi-module maven projects.")
   707  	}
   708  	defines := generateMavenFortifyDefines(&config, file)
   709  	executeOptions := maven.ExecuteOptions{
   710  		PomPath:             config.BuildDescriptorFile,
   711  		ProjectSettingsFile: config.ProjectSettingsFile,
   712  		GlobalSettingsFile:  config.GlobalSettingsFile,
   713  		M2Path:              config.M2Path,
   714  		Goals:               []string{"dependency:build-classpath", "package"},
   715  		Defines:             defines,
   716  		ReturnStdout:        false,
   717  	}
   718  	_, err := maven.Execute(&executeOptions, utils)
   719  	if err != nil {
   720  		log.Entry().WithError(err).Warnf("failed to determine classpath using Maven: %v", err)
   721  	}
   722  	return readAllClasspathFiles(file), nil
   723  }
   724  
   725  func autoresolveGradleClasspath(config fortifyExecuteScanOptions, file string, utils fortifyUtils) (string, error) {
   726  	gradleOptions := &gradle.ExecuteOptions{
   727  		Task:              "getClasspath",
   728  		UseWrapper:        true,
   729  		InitScriptContent: getClasspathScriptContent,
   730  		ProjectProperties: map[string]string{"filename": file},
   731  	}
   732  	if _, err := gradle.Execute(gradleOptions, utils); err != nil {
   733  		log.Entry().WithError(err).Warnf("failed to determine classpath using Gradle: %v", err)
   734  	}
   735  	return readAllClasspathFiles(file), nil
   736  }
   737  
   738  func generateMavenFortifyDefines(config *fortifyExecuteScanOptions, file string) []string {
   739  	defines := []string{
   740  		fmt.Sprintf("-Dmdep.outputFile=%v", file),
   741  		// Parameter to indicate to maven build that the fortify step is the trigger, can be used for optimizations
   742  		"-Dfortify",
   743  		"-DincludeScope=compile",
   744  		"-DskipTests",
   745  		"-Dmaven.javadoc.skip=true",
   746  		"--fail-at-end"}
   747  
   748  	if len(config.BuildDescriptorExcludeList) > 0 {
   749  		// From the documentation, these are file paths to a module's pom.xml.
   750  		// For MTA projects, we support pom.xml files here and skip others.
   751  		for _, exclude := range config.BuildDescriptorExcludeList {
   752  			if !strings.HasSuffix(exclude, "pom.xml") {
   753  				continue
   754  			}
   755  			exists, _ := piperutils.FileExists(exclude)
   756  			if !exists {
   757  				continue
   758  			}
   759  			moduleName := filepath.Dir(exclude)
   760  			if moduleName != "" {
   761  				defines = append(defines, "-pl", "!"+moduleName)
   762  			}
   763  		}
   764  	}
   765  
   766  	return defines
   767  }
   768  
   769  // readAllClasspathFiles tests whether the passed file is an absolute path. If not, it will glob for
   770  // all files under the current directory with the given file name and concatenate their contents.
   771  // Otherwise it will return the contents pointed to by the absolute path.
   772  func readAllClasspathFiles(file string) string {
   773  	var paths []string
   774  	if filepath.IsAbs(file) {
   775  		paths = []string{file}
   776  	} else {
   777  		paths, _ = doublestar.Glob(filepath.Join("**", file))
   778  		log.Entry().Debugf("Concatenating the class paths from %v", paths)
   779  	}
   780  	var contents string
   781  	const separator = ":"
   782  	for _, path := range paths {
   783  		contents += separator + readClasspathFile(path)
   784  	}
   785  	return removeDuplicates(contents, separator)
   786  }
   787  
   788  func readClasspathFile(file string) string {
   789  	data, err := ioutil.ReadFile(file)
   790  	if err != nil {
   791  		log.Entry().WithError(err).Warnf("failed to read classpath from file '%v'", file)
   792  	}
   793  	result := strings.TrimSpace(string(data))
   794  	if len(result) == 0 {
   795  		log.Entry().Warnf("classpath from file '%v' was empty", file)
   796  	}
   797  	return result
   798  }
   799  
   800  func removeDuplicates(contents, separator string) string {
   801  	if separator == "" || contents == "" {
   802  		return contents
   803  	}
   804  	entries := strings.Split(contents, separator)
   805  	entrySet := map[string]struct{}{}
   806  	contents = ""
   807  	for _, entry := range entries {
   808  		if entry == "" {
   809  			continue
   810  		}
   811  		_, contained := entrySet[entry]
   812  		if !contained {
   813  			entrySet[entry] = struct{}{}
   814  			contents += entry + separator
   815  		}
   816  	}
   817  	if contents != "" {
   818  		// Remove trailing "separator"
   819  		contents = contents[:len(contents)-len(separator)]
   820  	}
   821  	return contents
   822  }
   823  
   824  func triggerFortifyScan(config fortifyExecuteScanOptions, utils fortifyUtils, buildID, buildLabel, buildProject string) error {
   825  	var err error = nil
   826  	// Do special Python related prep
   827  	pipVersion := "pip3"
   828  	if config.PythonVersion != "python3" {
   829  		pipVersion = "pip2"
   830  	}
   831  
   832  	classpath := ""
   833  	if config.BuildTool == "maven" {
   834  		if config.AutodetectClasspath {
   835  			classpath, err = autoresolveMavenClasspath(config, classpathFileName, utils)
   836  			if err != nil {
   837  				return err
   838  			}
   839  		}
   840  		config.Translate, err = populateMavenGradleTranslate(&config, classpath)
   841  		if err != nil {
   842  			log.Entry().WithError(err).Warnf("failed to apply src ('%s') or exclude ('%s') parameter", config.Src, config.Exclude)
   843  		}
   844  	} else if config.BuildTool == "gradle" {
   845  		if config.AutodetectClasspath {
   846  			classpath, err = autoresolveGradleClasspath(config, classpathFileName, utils)
   847  			if err != nil {
   848  				return err
   849  			}
   850  		}
   851  		config.Translate, err = populateMavenGradleTranslate(&config, classpath)
   852  		if err != nil {
   853  			log.Entry().WithError(err).Warnf("failed to apply src ('%s') or exclude ('%s') parameter", config.Src, config.Exclude)
   854  		}
   855  	} else if config.BuildTool == "pip" {
   856  		if config.AutodetectClasspath {
   857  			separator := getSeparator()
   858  			script := fmt.Sprintf("import sys;p=sys.path;p.remove('');print('%v'.join(p))", separator)
   859  			classpath, err = autoresolvePipClasspath(config.PythonVersion, []string{"-c", script}, classpathFileName, utils)
   860  			if err != nil {
   861  				return errors.Wrap(err, "failed to autoresolve pip classpath")
   862  			}
   863  		}
   864  		// install the dev dependencies
   865  		if len(config.PythonRequirementsFile) > 0 {
   866  			context := map[string]string{}
   867  			cmdTemplate := []string{pipVersion, "install", "--user", "-r", config.PythonRequirementsFile}
   868  			cmdTemplate = append(cmdTemplate, tokenize(config.PythonRequirementsInstallSuffix)...)
   869  			executeTemplatedCommand(utils, cmdTemplate, context)
   870  		}
   871  
   872  		executeTemplatedCommand(utils, tokenize(config.PythonInstallCommand), map[string]string{"Pip": pipVersion})
   873  
   874  		config.Translate, err = populatePipTranslate(&config, classpath)
   875  		if err != nil {
   876  			log.Entry().WithError(err).Warnf("failed to apply pythonAdditionalPath ('%s') or src ('%s') parameter", config.PythonAdditionalPath, config.Src)
   877  		}
   878  
   879  	} else {
   880  		return fmt.Errorf("buildTool '%s' is not supported by this step", config.BuildTool)
   881  	}
   882  
   883  	err = translateProject(&config, utils, buildID, classpath)
   884  	if err != nil {
   885  		return err
   886  	}
   887  
   888  	return scanProject(&config, utils, buildID, buildLabel, buildProject)
   889  }
   890  
   891  func populatePipTranslate(config *fortifyExecuteScanOptions, classpath string) (string, error) {
   892  	if len(config.Translate) > 0 {
   893  		return config.Translate, nil
   894  	}
   895  
   896  	var translateList []map[string]interface{}
   897  	translateList = append(translateList, make(map[string]interface{}))
   898  
   899  	separator := getSeparator()
   900  
   901  	translateList[0]["pythonPath"] = classpath + separator +
   902  		getSuppliedOrDefaultListAsString(config.PythonAdditionalPath, []string{}, separator)
   903  	translateList[0]["src"] = getSuppliedOrDefaultListAsString(
   904  		config.Src, []string{"./**/*"}, ":")
   905  	translateList[0]["exclude"] = getSuppliedOrDefaultListAsString(
   906  		config.Exclude, []string{"./**/tests/**/*", "./**/setup.py"}, separator)
   907  
   908  	translateJSON, err := json.Marshal(translateList)
   909  
   910  	return string(translateJSON), err
   911  }
   912  
   913  func populateMavenGradleTranslate(config *fortifyExecuteScanOptions, classpath string) (string, error) {
   914  	if len(config.Translate) > 0 {
   915  		return config.Translate, nil
   916  	}
   917  
   918  	var translateList []map[string]interface{}
   919  	translateList = append(translateList, make(map[string]interface{}))
   920  	translateList[0]["classpath"] = classpath
   921  
   922  	setTranslateEntryIfNotEmpty(translateList[0], "src", ":", config.Src,
   923  		[]string{"**/*.xml", "**/*.html", "**/*.jsp", "**/*.js", "**/src/main/resources/**/*", "**/src/main/java/**/*", "**/target/main/java/**/*", "**/target/main/resources/**/*", "**/target/generated-sources/**/*"})
   924  
   925  	setTranslateEntryIfNotEmpty(translateList[0], "exclude", getSeparator(), config.Exclude, []string{"**/src/test/**/*"})
   926  
   927  	translateJSON, err := json.Marshal(translateList)
   928  
   929  	return string(translateJSON), err
   930  }
   931  
   932  func translateProject(config *fortifyExecuteScanOptions, utils fortifyUtils, buildID, classpath string) error {
   933  	var translateList []map[string]string
   934  	json.Unmarshal([]byte(config.Translate), &translateList)
   935  	log.Entry().Debugf("Translating with options: %v", translateList)
   936  	for _, translate := range translateList {
   937  		if len(classpath) > 0 {
   938  			translate["autoClasspath"] = classpath
   939  		}
   940  		err := handleSingleTranslate(config, utils, buildID, translate)
   941  		if err != nil {
   942  			return err
   943  		}
   944  	}
   945  	return nil
   946  }
   947  
   948  func handleSingleTranslate(config *fortifyExecuteScanOptions, command fortifyUtils, buildID string, t map[string]string) error {
   949  	if t != nil {
   950  		log.Entry().Debugf("Handling translate config %v", t)
   951  		translateOptions := []string{
   952  			"-verbose",
   953  			"-64",
   954  			"-b",
   955  			buildID,
   956  		}
   957  		translateOptions = append(translateOptions, tokenize(config.Memory)...)
   958  		translateOptions = appendToOptions(config, translateOptions, t)
   959  		log.Entry().Debugf("Running sourceanalyzer translate command with options %v", translateOptions)
   960  		err := command.RunExecutable("sourceanalyzer", translateOptions...)
   961  		if err != nil {
   962  			return errors.Wrapf(err, "failed to execute sourceanalyzer translate command with options %v", translateOptions)
   963  		}
   964  	} else {
   965  		log.Entry().Debug("Skipping translate with nil value")
   966  	}
   967  	return nil
   968  }
   969  
   970  func scanProject(config *fortifyExecuteScanOptions, command fortifyUtils, buildID, buildLabel, buildProject string) error {
   971  	var scanOptions = []string{
   972  		"-verbose",
   973  		"-64",
   974  		"-b",
   975  		buildID,
   976  		"-scan",
   977  	}
   978  	scanOptions = append(scanOptions, tokenize(config.Memory)...)
   979  	if config.QuickScan {
   980  		scanOptions = append(scanOptions, "-quick")
   981  	}
   982  	if len(config.AdditionalScanParameters) > 0 {
   983  		for _, scanParameter := range config.AdditionalScanParameters {
   984  			scanOptions = append(scanOptions, scanParameter)
   985  		}
   986  	}
   987  	if len(buildLabel) > 0 {
   988  		scanOptions = append(scanOptions, "-build-label", buildLabel)
   989  	}
   990  	if len(buildProject) > 0 {
   991  		scanOptions = append(scanOptions, "-build-project", buildProject)
   992  	}
   993  	scanOptions = append(scanOptions, "-logfile", "target/fortify-scan.log", "-f", "target/result.fpr")
   994  
   995  	err := command.RunExecutable("sourceanalyzer", scanOptions...)
   996  	if err != nil {
   997  		return errors.Wrapf(err, "failed to execute sourceanalyzer scan command with scanOptions %v", scanOptions)
   998  	}
   999  	return nil
  1000  }
  1001  
  1002  func determinePullRequestMerge(config fortifyExecuteScanOptions) (string, string) {
  1003  	author := ""
  1004  	//TODO provide parameter for trusted certs
  1005  	ctx, client, err := piperGithub.NewClient(config.GithubToken, config.GithubAPIURL, "", []string{})
  1006  	if err == nil && ctx != nil && client != nil {
  1007  		prID, author, err := determinePullRequestMergeGithub(ctx, config, client.PullRequests)
  1008  		if err != nil {
  1009  			log.Entry().WithError(err).Warn("Failed to get PR metadata via GitHub client")
  1010  		} else {
  1011  			return prID, author
  1012  		}
  1013  	} else {
  1014  		log.Entry().WithError(err).Warn("Failed to instantiate GitHub client to get PR metadata")
  1015  	}
  1016  
  1017  	log.Entry().Infof("Trying to determine PR ID in commit message: %v", config.CommitMessage)
  1018  	r, _ := regexp.Compile(config.PullRequestMessageRegex)
  1019  	matches := r.FindSubmatch([]byte(config.CommitMessage))
  1020  	if matches != nil && len(matches) > 1 {
  1021  		return string(matches[config.PullRequestMessageRegexGroup]), author
  1022  	}
  1023  	return "0", ""
  1024  }
  1025  
  1026  func determinePullRequestMergeGithub(ctx context.Context, config fortifyExecuteScanOptions, pullRequestServiceInstance pullRequestService) (string, string, error) {
  1027  	number := "0"
  1028  	author := ""
  1029  	options := github.PullRequestListOptions{State: "closed", Sort: "updated", Direction: "desc"}
  1030  	prList, _, err := pullRequestServiceInstance.ListPullRequestsWithCommit(ctx, config.Owner, config.Repository, config.CommitID, &options)
  1031  	if err == nil && prList != nil && len(prList) > 0 {
  1032  		number = fmt.Sprintf("%v", prList[0].GetNumber())
  1033  		if prList[0].GetUser() != nil {
  1034  			author = prList[0].GetUser().GetLogin()
  1035  		}
  1036  		return number, author, nil
  1037  	} else {
  1038  		log.Entry().Infof("Unable to resolve PR via commit ID: %v", config.CommitID)
  1039  	}
  1040  	return number, author, err
  1041  }
  1042  
  1043  func appendToOptions(config *fortifyExecuteScanOptions, options []string, t map[string]string) []string {
  1044  	switch config.BuildTool {
  1045  	case "windows":
  1046  		if len(t["aspnetcore"]) > 0 {
  1047  			options = append(options, "-aspnetcore")
  1048  		}
  1049  		if len(t["dotNetCoreVersion"]) > 0 {
  1050  			options = append(options, "-dotnet-core-version", t["dotNetCoreVersion"])
  1051  		}
  1052  		if len(t["libDirs"]) > 0 {
  1053  			options = append(options, "-libdirs", t["libDirs"])
  1054  		}
  1055  
  1056  	case "maven", "gradle":
  1057  		if len(t["autoClasspath"]) > 0 {
  1058  			options = append(options, "-cp", t["autoClasspath"])
  1059  		} else if len(t["classpath"]) > 0 {
  1060  			options = append(options, "-cp", t["classpath"])
  1061  		} else {
  1062  			log.Entry().Debugf("no field 'autoClasspath' or 'classpath' in map or both empty")
  1063  		}
  1064  		if len(t["extdirs"]) > 0 {
  1065  			options = append(options, "-extdirs", t["extdirs"])
  1066  		}
  1067  		if len(t["javaBuildDir"]) > 0 {
  1068  			options = append(options, "-java-build-dir", t["javaBuildDir"])
  1069  		}
  1070  		if len(t["source"]) > 0 {
  1071  			options = append(options, "-source", t["source"])
  1072  		}
  1073  		if len(t["jdk"]) > 0 {
  1074  			options = append(options, "-jdk", t["jdk"])
  1075  		}
  1076  		if len(t["sourcepath"]) > 0 {
  1077  			options = append(options, "-sourcepath", t["sourcepath"])
  1078  		}
  1079  
  1080  	case "pip":
  1081  		if len(t["autoClasspath"]) > 0 {
  1082  			options = append(options, "-python-path", t["autoClasspath"])
  1083  		} else if len(t["pythonPath"]) > 0 {
  1084  			options = append(options, "-python-path", t["pythonPath"])
  1085  		}
  1086  		if len(t["djangoTemplatDirs"]) > 0 {
  1087  			options = append(options, "-django-template-dirs", t["djangoTemplatDirs"])
  1088  		}
  1089  
  1090  	default:
  1091  		return options
  1092  	}
  1093  
  1094  	if len(t["exclude"]) > 0 {
  1095  		options = append(options, "-exclude", t["exclude"])
  1096  	}
  1097  	return append(options, strings.Split(t["src"], ":")...)
  1098  }
  1099  
  1100  func getSuppliedOrDefaultList(suppliedList, defaultList []string) []string {
  1101  	if len(suppliedList) > 0 {
  1102  		return suppliedList
  1103  	}
  1104  	return defaultList
  1105  }
  1106  
  1107  func getSuppliedOrDefaultListAsString(suppliedList, defaultList []string, separator string) string {
  1108  	effectiveList := getSuppliedOrDefaultList(suppliedList, defaultList)
  1109  	return strings.Join(effectiveList, separator)
  1110  }
  1111  
  1112  // setTranslateEntryIfNotEmpty builds a string from either the user-supplied list, or the default list,
  1113  // by joining the entries with the given separator. If the resulting string is not empty, it will be
  1114  // placed as an entry in the provided map under the given key.
  1115  func setTranslateEntryIfNotEmpty(translate map[string]interface{}, key, separator string, suppliedList, defaultList []string) {
  1116  	value := getSuppliedOrDefaultListAsString(suppliedList, defaultList, separator)
  1117  	if value != "" {
  1118  		translate[key] = value
  1119  	}
  1120  }
  1121  
  1122  // getSeparator returns the separator string depending on the host platform. This assumes that
  1123  // Piper executes the Fortify command line tools within the same OS platform as it is running on itself.
  1124  func getSeparator() string {
  1125  	if runtime.GOOS == "windows" {
  1126  		return ";"
  1127  	}
  1128  	return ":"
  1129  }
  1130  
  1131  func createToolRecordFortify(workspace string, config fortifyExecuteScanOptions, projectID int64, projectName string, projectVersionID int64, projectVersion string) (string, error) {
  1132  	record := toolrecord.New(workspace, "fortify", config.ServerURL)
  1133  	// Project
  1134  	err := record.AddKeyData("project",
  1135  		strconv.FormatInt(projectID, 10),
  1136  		projectName,
  1137  		"")
  1138  	if err != nil {
  1139  		return "", err
  1140  	}
  1141  	// projectVersion
  1142  	projectVersionURL := config.ServerURL + "/html/ssc/version/" + strconv.FormatInt(projectVersionID, 10)
  1143  	err = record.AddKeyData("projectVersion",
  1144  		strconv.FormatInt(projectVersionID, 10),
  1145  		projectVersion,
  1146  		projectVersionURL)
  1147  	if err != nil {
  1148  		return "", err
  1149  	}
  1150  	err = record.Persist()
  1151  	if err != nil {
  1152  		return "", err
  1153  	}
  1154  	return record.GetFileName(), nil
  1155  }