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