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

     1  package cmd
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"path/filepath"
     9  	"regexp"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	bd "github.com/SAP/jenkins-library/pkg/blackduck"
    16  	piperGithub "github.com/SAP/jenkins-library/pkg/github"
    17  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    18  	"github.com/SAP/jenkins-library/pkg/maven"
    19  	"github.com/SAP/jenkins-library/pkg/reporting"
    20  	"github.com/SAP/jenkins-library/pkg/versioning"
    21  	"github.com/pkg/errors"
    22  
    23  	"github.com/SAP/jenkins-library/pkg/command"
    24  	"github.com/SAP/jenkins-library/pkg/log"
    25  	"github.com/SAP/jenkins-library/pkg/piperutils"
    26  	"github.com/SAP/jenkins-library/pkg/telemetry"
    27  	"github.com/SAP/jenkins-library/pkg/toolrecord"
    28  )
    29  
    30  type detectUtils interface {
    31  	piperutils.FileUtils
    32  
    33  	GetExitCode() int
    34  	GetOsEnv() []string
    35  	Stdout(out io.Writer)
    36  	Stderr(err io.Writer)
    37  	SetDir(dir string)
    38  	SetEnv(env []string)
    39  	RunExecutable(e string, p ...string) error
    40  	RunShell(shell, script string) error
    41  
    42  	DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error
    43  
    44  	CreateIssue(ghCreateIssueOptions *piperGithub.CreateIssueOptions) error
    45  }
    46  
    47  type detectUtilsBundle struct {
    48  	*command.Command
    49  	*piperutils.Files
    50  	*piperhttp.Client
    51  }
    52  
    53  // CreateIssue supplies capability for GitHub issue creation
    54  func (d *detectUtilsBundle) CreateIssue(ghCreateIssueOptions *piperGithub.CreateIssueOptions) error {
    55  	return piperGithub.CreateIssue(ghCreateIssueOptions)
    56  }
    57  
    58  type blackduckSystem struct {
    59  	Client bd.Client
    60  }
    61  
    62  func newDetectUtils() detectUtils {
    63  	utils := detectUtilsBundle{
    64  		Command: &command.Command{
    65  			ErrorCategoryMapping: map[string][]string{
    66  				log.ErrorCompliance.String(): {
    67  					"FAILURE_POLICY_VIOLATION - Detect found policy violations.",
    68  				},
    69  				log.ErrorConfiguration.String(): {
    70  					"FAILURE_CONFIGURATION - Detect was unable to start due to issues with it's configuration.",
    71  					"FAILURE_DETECTOR - Detect had one or more detector failures while extracting dependencies. Check that all projects build and your environment is configured correctly.",
    72  					"FAILURE_SCAN - Detect was unable to run the signature scanner against your source. Check your configuration.",
    73  				},
    74  				log.ErrorInfrastructure.String(): {
    75  					"FAILURE_PROXY_CONNECTIVITY - Detect was unable to use the configured proxy. Check your configuration and connection.",
    76  					"FAILURE_BLACKDUCK_CONNECTIVITY - Detect was unable to connect to Black Duck. Check your configuration and connection.",
    77  					"FAILURE_POLARIS_CONNECTIVITY - Detect was unable to connect to Polaris. Check your configuration and connection.",
    78  				},
    79  				log.ErrorService.String(): {
    80  					"FAILURE_TIMEOUT - Detect could not wait for actions to be completed on Black Duck. Check your Black Duck server or increase your timeout.",
    81  					"FAILURE_DETECTOR_REQUIRED - Detect did not run all of the required detectors. Fix detector issues or disable required detectors.",
    82  					"FAILURE_BLACKDUCK_VERSION_NOT_SUPPORTED - Detect attempted an operation that was not supported by your version of Black Duck. Ensure your Black Duck is compatible with this version of detect.",
    83  					"FAILURE_BLACKDUCK_FEATURE_ERROR - Detect encountered an error while attempting an operation on Black Duck. Ensure your Black Duck is compatible with this version of detect.",
    84  					"FAILURE_GENERAL_ERROR - Detect encountered a known error, details of the error are provided.",
    85  					"FAILURE_UNKNOWN_ERROR - Detect encountered an unknown error.",
    86  				},
    87  			},
    88  		},
    89  		Files:  &piperutils.Files{},
    90  		Client: &piperhttp.Client{},
    91  	}
    92  	utils.Stdout(log.Writer())
    93  	utils.Stderr(log.Writer())
    94  	return &utils
    95  }
    96  
    97  func newBlackduckSystem(config detectExecuteScanOptions) *blackduckSystem {
    98  	sys := blackduckSystem{
    99  		Client: bd.NewClient(config.Token, config.ServerURL, &piperhttp.Client{}),
   100  	}
   101  	return &sys
   102  }
   103  
   104  func detectExecuteScan(config detectExecuteScanOptions, _ *telemetry.CustomData, influx *detectExecuteScanInflux) {
   105  	influx.step_data.fields.detect = false
   106  	utils := newDetectUtils()
   107  	err := runDetect(config, utils, influx)
   108  
   109  	if err != nil {
   110  		log.Entry().
   111  			WithError(err).
   112  			Fatal("failed to execute detect scan")
   113  	}
   114  
   115  	influx.step_data.fields.detect = true
   116  }
   117  
   118  func runDetect(config detectExecuteScanOptions, utils detectUtils, influx *detectExecuteScanInflux) error {
   119  	// detect execution details, see https://synopsys.atlassian.net/wiki/spaces/INTDOCS/pages/88440888/Sample+Synopsys+Detect+Scan+Configuration+Scenarios+for+Black+Duck
   120  	err := getDetectScript(config, utils)
   121  	if err != nil {
   122  		return fmt.Errorf("failed to download 'detect.sh' script: %w", err)
   123  	}
   124  	defer func() {
   125  		err := utils.FileRemove("detect.sh")
   126  		if err != nil {
   127  			log.Entry().Warnf("failed to delete 'detect.sh' script: %v", err)
   128  		}
   129  	}()
   130  	err = utils.Chmod("detect.sh", 0700)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	if config.InstallArtifacts {
   136  		err := maven.InstallMavenArtifacts(&maven.EvaluateOptions{
   137  			M2Path:              config.M2Path,
   138  			ProjectSettingsFile: config.ProjectSettingsFile,
   139  			GlobalSettingsFile:  config.GlobalSettingsFile,
   140  		}, utils)
   141  		if err != nil {
   142  			return err
   143  		}
   144  	}
   145  
   146  	args := []string{"./detect.sh"}
   147  	args, err = addDetectArgs(args, config, utils)
   148  	if err != nil {
   149  		return err
   150  	}
   151  	script := strings.Join(args, " ")
   152  
   153  	envs := []string{"BLACKDUCK_SKIP_PHONE_HOME=true"}
   154  	envs = append(envs, config.CustomEnvironmentVariables...)
   155  
   156  	utils.SetDir(".")
   157  	utils.SetEnv(envs)
   158  
   159  	err = utils.RunShell("/bin/bash", script)
   160  	blackduckSystem := newBlackduckSystem(config)
   161  	reportingErr := postScanChecksAndReporting(config, influx, utils, blackduckSystem)
   162  	if reportingErr != nil {
   163  		if strings.Contains(reportingErr.Error(), "License Policy Violations found") {
   164  			log.Entry().Errorf("License Policy Violations found")
   165  			log.SetErrorCategory(log.ErrorCompliance)
   166  			if err == nil && !piperutils.ContainsStringPart(config.FailOn, "NONE") {
   167  				err = errors.New("License Policy Violations found")
   168  			}
   169  		} else {
   170  			log.Entry().Warnf("Failed to generate reports: %v", reportingErr)
   171  		}
   172  	}
   173  	if err != nil {
   174  		// Setting error category based on exit code
   175  		mapErrorCategory(utils.GetExitCode())
   176  
   177  		// Error code mapping with more human readable text
   178  		err = errors.Wrapf(err, exitCodeMapping(utils.GetExitCode()))
   179  	}
   180  	// create Toolrecord file
   181  	toolRecordFileName, toolRecordErr := createToolRecordDetect("./", config, blackduckSystem)
   182  	if toolRecordErr != nil {
   183  		// do not fail until the framework is well established
   184  		log.Entry().Warning("TR_DETECT: Failed to create toolrecord file "+toolRecordFileName, err)
   185  	}
   186  	return err
   187  }
   188  
   189  // Get proper error category
   190  func mapErrorCategory(exitCodeKey int) {
   191  	switch exitCodeKey {
   192  	case 0:
   193  		//In case detect exits successfully, we rely on the function 'postScanChecksAndReporting' to determine the error category
   194  		//hence this method doesnt need to set an error category or go to 'default' case
   195  		break
   196  	case 1:
   197  		log.SetErrorCategory(log.ErrorInfrastructure)
   198  	case 2:
   199  		log.SetErrorCategory(log.ErrorService)
   200  	case 3:
   201  		log.SetErrorCategory(log.ErrorCompliance)
   202  	case 4:
   203  		log.SetErrorCategory(log.ErrorInfrastructure)
   204  	case 5:
   205  		log.SetErrorCategory(log.ErrorConfiguration)
   206  	case 6:
   207  		log.SetErrorCategory(log.ErrorConfiguration)
   208  	case 7:
   209  		log.SetErrorCategory(log.ErrorConfiguration)
   210  	case 9:
   211  		log.SetErrorCategory(log.ErrorService)
   212  	case 10:
   213  		log.SetErrorCategory(log.ErrorService)
   214  	case 11:
   215  		log.SetErrorCategory(log.ErrorService)
   216  	case 12:
   217  		log.SetErrorCategory(log.ErrorInfrastructure)
   218  	case 99:
   219  		log.SetErrorCategory(log.ErrorService)
   220  	case 100:
   221  		log.SetErrorCategory(log.ErrorUndefined)
   222  	default:
   223  		log.SetErrorCategory(log.ErrorUndefined)
   224  	}
   225  }
   226  
   227  // Exit codes/error code mapping
   228  func exitCodeMapping(exitCodeKey int) string {
   229  
   230  	exitCodes := map[int]string{
   231  		0:   "Detect Scan completed successfully",
   232  		1:   "FAILURE_BLACKDUCK_CONNECTIVITY => Detect was unable to connect to Black Duck. Check your configuration and connection.",
   233  		2:   "FAILURE_TIMEOUT => Detect could not wait for actions to be completed on Black Duck. Check your Black Duck server or increase your timeout.",
   234  		3:   "FAILURE_POLICY_VIOLATION => Detect found policy violations.",
   235  		4:   "FAILURE_PROXY_CONNECTIVITY => Detect was unable to use the configured proxy. Check your configuration and connection.",
   236  		5:   "FAILURE_DETECTOR => Detect had one or more detector failures while extracting dependencies. Check that all projects build and your environment is configured correctly.",
   237  		6:   "FAILURE_SCAN => Detect was unable to run the signature scanner against your source. Check your configuration.",
   238  		7:   "FAILURE_CONFIGURATION => Detect was unable to start because of a configuration issue. Check and fix your configuration.",
   239  		9:   "FAILURE_DETECTOR_REQUIRED => Detect did not run all of the required detectors. Fix detector issues or disable required detectors.",
   240  		10:  "FAILURE_BLACKDUCK_VERSION_NOT_SUPPORTED => Detect attempted an operation that was not supported by your version of Black Duck. Ensure your Black Duck is compatible with this version of detect.",
   241  		11:  "FAILURE_BLACKDUCK_FEATURE_ERROR => Detect encountered an error while attempting an operation on Black Duck. Ensure your Black Duck is compatible with this version of detect.",
   242  		12:  "FAILURE_POLARIS_CONNECTIVITY => Detect was unable to connect to Polaris. Check your configuration and connection.",
   243  		99:  "FAILURE_GENERAL_ERROR => Detect encountered a known error, details of the error are provided.",
   244  		100: "FAILURE_UNKNOWN_ERROR => Detect encountered an unknown error.",
   245  	}
   246  
   247  	if _, isKeyExists := exitCodes[exitCodeKey]; isKeyExists {
   248  		return exitCodes[exitCodeKey]
   249  	}
   250  
   251  	return "[" + strconv.Itoa(exitCodeKey) + "]: Not known exit code key"
   252  }
   253  
   254  func getDetectScript(config detectExecuteScanOptions, utils detectUtils) error {
   255  	if config.ScanOnChanges {
   256  		log.Entry().Infof("Using Detect Rescan script")
   257  		return utils.DownloadFile("https://raw.githubusercontent.com/blackducksoftware/detect_rescan/master/detect_rescan.sh", "detect.sh", nil, nil)
   258  	}
   259  	env := utils.GetOsEnv()
   260  	env = append(env, config.CustomEnvironmentVariables...)
   261  	if piperutils.ContainsStringPart(env, "DETECT_LATEST_RELEASE_VERSION") {
   262  		releaseVersion := ""
   263  		for _, i := range env {
   264  			if strings.Contains(i, "DETECT_LATEST_RELEASE_VERSION") {
   265  				releaseVersion = strings.Split(i, "=")[1]
   266  			}
   267  		}
   268  		log.Entry().Infof("Using detect script Version %v ", releaseVersion)
   269  		detect6, _ := regexp.MatchString("6\\.\\d\\.\\d", releaseVersion)
   270  		if detect6 {
   271  			log.Entry().Infof("Downloading Detect 6.x")
   272  			return utils.DownloadFile("https://detect.synopsys.com/detect.sh", "detect.sh", nil, nil)
   273  		}
   274  	}
   275  	log.Entry().Infof("Downloading Detect7")
   276  	return utils.DownloadFile("https://detect.synopsys.com/detect7.sh", "detect.sh", nil, nil)
   277  }
   278  
   279  func addDetectArgs(args []string, config detectExecuteScanOptions, utils detectUtils) ([]string, error) {
   280  	detectVersionName := getVersionName(config)
   281  	//Split on spaces, the scanPropeties, so that each property is available as a single string
   282  	//instead of all properties being part of a single string
   283  	config.ScanProperties = piperutils.SplitAndTrim(config.ScanProperties, " ")
   284  
   285  	if config.ScanOnChanges {
   286  		args = append(args, "--report")
   287  		config.Unmap = false
   288  	}
   289  
   290  	if config.Unmap {
   291  		if !piperutils.ContainsString(config.ScanProperties, "--detect.project.codelocation.unmap=true") {
   292  			args = append(args, fmt.Sprintf("--detect.project.codelocation.unmap=true"))
   293  		}
   294  		config.ScanProperties, _ = piperutils.RemoveAll(config.ScanProperties, "--detect.project.codelocation.unmap=false")
   295  	} else {
   296  		//When unmap is set to false, any occurances of unmap=true from scanProperties must be removed
   297  		config.ScanProperties, _ = piperutils.RemoveAll(config.ScanProperties, "--detect.project.codelocation.unmap=true")
   298  	}
   299  
   300  	args = append(args, config.ScanProperties...)
   301  
   302  	args = append(args, fmt.Sprintf("--blackduck.url=%v", config.ServerURL))
   303  	args = append(args, fmt.Sprintf("--blackduck.api.token=%v", config.Token))
   304  	// ProjectNames, VersionName, GroupName etc can contain spaces and need to be escaped using double quotes in CLI
   305  	// Hence the string need to be surrounded by \"
   306  	args = append(args, fmt.Sprintf("\"--detect.project.name='%v'\"", config.ProjectName))
   307  	args = append(args, fmt.Sprintf("\"--detect.project.version.name='%v'\"", detectVersionName))
   308  
   309  	// Groups parameter is added only when there is atleast one non-empty groupname provided
   310  	if len(config.Groups) > 0 && len(config.Groups[0]) > 0 {
   311  		args = append(args, fmt.Sprintf("\"--detect.project.user.groups='%v'\"", strings.Join(config.Groups, ",")))
   312  	}
   313  
   314  	// Atleast 1, non-empty category to fail on must be provided
   315  	if len(config.FailOn) > 0 && len(config.FailOn[0]) > 0 {
   316  		args = append(args, fmt.Sprintf("--detect.policy.check.fail.on.severities=%v", strings.Join(config.FailOn, ",")))
   317  	}
   318  
   319  	codelocation := config.CodeLocation
   320  	if len(codelocation) == 0 && len(config.ProjectName) > 0 {
   321  		codelocation = fmt.Sprintf("%v/%v", config.ProjectName, detectVersionName)
   322  	}
   323  	args = append(args, fmt.Sprintf("\"--detect.code.location.name='%v'\"", codelocation))
   324  
   325  	if len(config.ScanPaths) > 0 && len(config.ScanPaths[0]) > 0 {
   326  		args = append(args, fmt.Sprintf("--detect.blackduck.signature.scanner.paths=%v", strings.Join(config.ScanPaths, ",")))
   327  	}
   328  
   329  	if len(config.DependencyPath) > 0 {
   330  		args = append(args, fmt.Sprintf("--detect.source.path=%v", config.DependencyPath))
   331  	} else {
   332  		args = append(args, fmt.Sprintf("--detect.source.path='.'"))
   333  	}
   334  
   335  	if len(config.IncludedPackageManagers) > 0 {
   336  		args = append(args, fmt.Sprintf("--detect.included.detector.types=%v", strings.ToUpper(strings.Join(config.IncludedPackageManagers, ","))))
   337  	}
   338  
   339  	if len(config.ExcludedPackageManagers) > 0 {
   340  		args = append(args, fmt.Sprintf("--detect.excluded.detector.types=%v", strings.ToUpper(strings.Join(config.ExcludedPackageManagers, ","))))
   341  	}
   342  
   343  	if len(config.MavenExcludedScopes) > 0 {
   344  		args = append(args, fmt.Sprintf("--detect.maven.excluded.scopes=%v", strings.ToLower(strings.Join(config.MavenExcludedScopes, ","))))
   345  	}
   346  
   347  	if len(config.DetectTools) > 0 {
   348  		args = append(args, fmt.Sprintf("--detect.tools=%v", strings.Join(config.DetectTools, ",")))
   349  	}
   350  
   351  	mavenArgs, err := maven.DownloadAndGetMavenParameters(config.GlobalSettingsFile, config.ProjectSettingsFile, utils)
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  
   356  	if len(config.M2Path) > 0 {
   357  		absolutePath, err := utils.Abs(config.M2Path)
   358  		if err != nil {
   359  			return nil, err
   360  		}
   361  		mavenArgs = append(mavenArgs, fmt.Sprintf("-Dmaven.repo.local=%v", absolutePath))
   362  	}
   363  
   364  	if len(mavenArgs) > 0 {
   365  		args = append(args, fmt.Sprintf("\"--detect.maven.build.command='%v'\"", strings.Join(mavenArgs, " ")))
   366  	}
   367  
   368  	return args, nil
   369  }
   370  
   371  func getVersionName(config detectExecuteScanOptions) string {
   372  	detectVersionName := config.CustomScanVersion
   373  	if len(detectVersionName) > 0 {
   374  		log.Entry().Infof("Using custom version: %v", detectVersionName)
   375  	} else {
   376  		detectVersionName = versioning.ApplyVersioningModel(config.VersioningModel, config.Version)
   377  	}
   378  	return detectVersionName
   379  }
   380  
   381  func createVulnerabilityReport(config detectExecuteScanOptions, vulns *bd.Vulnerabilities, influx *detectExecuteScanInflux, sys *blackduckSystem) reporting.ScanReport {
   382  	versionName := getVersionName(config)
   383  	versionUrl, _ := sys.Client.GetProjectVersionLink(config.ProjectName, versionName)
   384  	scanReport := reporting.ScanReport{
   385  		ReportTitle: "BlackDuck Security Vulnerability Report",
   386  		Subheaders: []reporting.Subheader{
   387  			{Description: "BlackDuck Project Name ", Details: config.ProjectName},
   388  			{Description: "BlackDuck Project Version ", Details: fmt.Sprintf("<a href='%v'>%v</a>", versionUrl, versionName)},
   389  		},
   390  		Overview: []reporting.OverviewRow{
   391  			{Description: "Total number of vulnerabilities ", Details: fmt.Sprint(influx.detect_data.fields.vulnerabilities)},
   392  			{Description: "Total number of Critical/High vulnerabilties ", Details: fmt.Sprint(influx.detect_data.fields.major_vulnerabilities)},
   393  		},
   394  		SuccessfulScan: influx.detect_data.fields.major_vulnerabilities == 0,
   395  		ReportTime:     time.Now(),
   396  	}
   397  
   398  	detailTable := reporting.ScanDetailTable{
   399  		NoRowsMessage: "No publicly known vulnerabilities detected",
   400  		Headers: []string{
   401  			"Vulnerability Name",
   402  			"Severity",
   403  			"Overall Score",
   404  			"Base Score",
   405  			"Component Name",
   406  			"Component Version",
   407  			"Description",
   408  			"Status",
   409  		},
   410  		WithCounter:   true,
   411  		CounterHeader: "Entry#",
   412  	}
   413  
   414  	vulnItems := vulns.Items
   415  	sort.Slice(vulnItems, func(i, j int) bool {
   416  		return vulnItems[i].OverallScore > vulnItems[j].OverallScore
   417  	})
   418  
   419  	for _, vuln := range vulnItems {
   420  		row := reporting.ScanRow{}
   421  		row.AddColumn(vuln.VulnerabilityWithRemediation.VulnerabilityName, 0)
   422  		row.AddColumn(vuln.VulnerabilityWithRemediation.Severity, 0)
   423  
   424  		var scoreStyle reporting.ColumnStyle = reporting.Yellow
   425  		if isMajorVulnerability(vuln) {
   426  			scoreStyle = reporting.Red
   427  		}
   428  		if !isActiveVulnerability(vuln) {
   429  			scoreStyle = reporting.Grey
   430  		}
   431  		row.AddColumn(vuln.VulnerabilityWithRemediation.OverallScore, scoreStyle)
   432  		row.AddColumn(vuln.VulnerabilityWithRemediation.BaseScore, 0)
   433  		row.AddColumn(vuln.Name, 0)
   434  		row.AddColumn(vuln.Version, 0)
   435  		row.AddColumn(vuln.VulnerabilityWithRemediation.Description, 0)
   436  		row.AddColumn(vuln.VulnerabilityWithRemediation.RemediationStatus, 0)
   437  
   438  		detailTable.Rows = append(detailTable.Rows, row)
   439  	}
   440  
   441  	scanReport.DetailTable = detailTable
   442  	return scanReport
   443  }
   444  
   445  func isActiveVulnerability(v bd.Vulnerability) bool {
   446  	switch v.VulnerabilityWithRemediation.RemediationStatus {
   447  	case "NEW":
   448  		return true
   449  	case "REMEDIATION_REQUIRED":
   450  		return true
   451  	case "NEEDS_REVIEW":
   452  		return true
   453  	default:
   454  		return false
   455  	}
   456  }
   457  
   458  func isMajorVulnerability(v bd.Vulnerability) bool {
   459  	switch v.VulnerabilityWithRemediation.Severity {
   460  	case "CRITICAL":
   461  		return true
   462  	case "HIGH":
   463  		return true
   464  	default:
   465  		return false
   466  	}
   467  }
   468  
   469  func postScanChecksAndReporting(config detectExecuteScanOptions, influx *detectExecuteScanInflux, utils detectUtils, sys *blackduckSystem) error {
   470  	errorsOccured := []string{}
   471  	vulns, _, err := getVulnsAndComponents(config, influx, sys)
   472  	if err != nil {
   473  		return errors.Wrap(err, "failed to fetch vulnerabilities")
   474  	}
   475  
   476  	if config.CreateResultIssue && len(config.GithubToken) > 0 && len(config.GithubAPIURL) > 0 && len(config.Owner) > 0 && len(config.Repository) > 0 {
   477  		log.Entry().Debugf("Creating result issues for %v alert(s)", len(vulns.Items))
   478  		issueDetails := make([]reporting.IssueDetail, len(vulns.Items))
   479  		piperutils.CopyAtoB(vulns.Items, issueDetails)
   480  		err = reporting.UploadMultipleReportsToGithub(&issueDetails, config.GithubToken, config.GithubAPIURL, config.Owner, config.Repository, config.Assignees, config.CustomTLSCertificateLinks, utils)
   481  		if err != nil {
   482  			errorsOccured = append(errorsOccured, fmt.Sprint(err))
   483  		}
   484  	}
   485  
   486  	sarif := bd.CreateSarifResultFile(vulns)
   487  	paths, err := bd.WriteSarifFile(sarif, utils)
   488  	if err != nil {
   489  		errorsOccured = append(errorsOccured, fmt.Sprint(err))
   490  	}
   491  
   492  	scanReport := createVulnerabilityReport(config, vulns, influx, sys)
   493  	vulnerabilityReportPaths, err := bd.WriteVulnerabilityReports(scanReport, utils)
   494  	if err != nil {
   495  		errorsOccured = append(errorsOccured, fmt.Sprint(err))
   496  	}
   497  	paths = append(paths, vulnerabilityReportPaths...)
   498  
   499  	policyStatus, err := getPolicyStatus(config, influx, sys)
   500  	policyReport := createPolicyStatusReport(config, policyStatus, influx, sys)
   501  	policyReportPaths, err := writePolicyStatusReports(policyReport, config, utils)
   502  	if err != nil {
   503  		errorsOccured = append(errorsOccured, fmt.Sprint(err))
   504  	}
   505  	paths = append(paths, policyReportPaths...)
   506  
   507  	piperutils.PersistReportsAndLinks("detectExecuteScan", "", paths, nil)
   508  	if err != nil {
   509  		errorsOccured = append(errorsOccured, fmt.Sprint(err))
   510  	}
   511  
   512  	err, violationCount := writeIpPolicyJson(config, utils, paths, sys)
   513  	if err != nil {
   514  		errorsOccured = append(errorsOccured, fmt.Sprint(err))
   515  	}
   516  
   517  	if violationCount > 0 {
   518  		log.SetErrorCategory(log.ErrorCompliance)
   519  		errorsOccured = append(errorsOccured, fmt.Sprint("License Policy Violations found"))
   520  	}
   521  
   522  	if len(errorsOccured) > 0 {
   523  		return fmt.Errorf(strings.Join(errorsOccured, ": "))
   524  	}
   525  
   526  	return nil
   527  }
   528  
   529  func getVulnsAndComponents(config detectExecuteScanOptions, influx *detectExecuteScanInflux, sys *blackduckSystem) (*bd.Vulnerabilities, *bd.Components, error) {
   530  	detectVersionName := getVersionName(config)
   531  	vulns, err := sys.Client.GetVulnerabilities(config.ProjectName, detectVersionName)
   532  	if err != nil {
   533  		return nil, nil, err
   534  	}
   535  
   536  	majorVulns := 0
   537  	activeVulns := 0
   538  	for _, vuln := range vulns.Items {
   539  		if isActiveVulnerability(vuln) {
   540  			activeVulns++
   541  			if isMajorVulnerability(vuln) {
   542  				majorVulns++
   543  			}
   544  		}
   545  	}
   546  	influx.detect_data.fields.vulnerabilities = activeVulns
   547  	influx.detect_data.fields.major_vulnerabilities = majorVulns
   548  	influx.detect_data.fields.minor_vulnerabilities = activeVulns - majorVulns
   549  
   550  	components, err := sys.Client.GetComponents(config.ProjectName, detectVersionName)
   551  	if err != nil {
   552  		return vulns, nil, err
   553  	}
   554  	influx.detect_data.fields.components = components.TotalCount
   555  
   556  	return vulns, components, nil
   557  }
   558  
   559  func getPolicyStatus(config detectExecuteScanOptions, influx *detectExecuteScanInflux, sys *blackduckSystem) (*bd.PolicyStatus, error) {
   560  	policyStatus, err := sys.Client.GetPolicyStatus(config.ProjectName, getVersionName(config))
   561  	if err != nil {
   562  		return nil, err
   563  	}
   564  
   565  	totalViolations := 0
   566  	for _, level := range policyStatus.SeverityLevels {
   567  		totalViolations += level.Value
   568  	}
   569  	influx.detect_data.fields.policy_violations = totalViolations
   570  
   571  	return policyStatus, nil
   572  }
   573  
   574  func createPolicyStatusReport(config detectExecuteScanOptions, policyStatus *bd.PolicyStatus, influx *detectExecuteScanInflux, sys *blackduckSystem) reporting.ScanReport {
   575  	versionName := getVersionName(config)
   576  	versionUrl, _ := sys.Client.GetProjectVersionLink(config.ProjectName, versionName)
   577  	policyReport := reporting.ScanReport{
   578  		ReportTitle: "BlackDuck Policy Violations Report",
   579  		Subheaders: []reporting.Subheader{
   580  			{Description: "BlackDuck project name ", Details: config.ProjectName},
   581  			{Description: "BlackDuck project version name", Details: fmt.Sprintf("<a href='%v'>%v</a>", versionUrl, versionName)},
   582  		},
   583  		Overview: []reporting.OverviewRow{
   584  			{Description: "Overall Policy Violation Status", Details: policyStatus.OverallStatus},
   585  			{Description: "Total Number of Policy Vioaltions", Details: fmt.Sprint(influx.detect_data.fields.policy_violations)},
   586  		},
   587  		SuccessfulScan: influx.detect_data.fields.policy_violations > 0,
   588  		ReportTime:     time.Now(),
   589  	}
   590  
   591  	detailTable := reporting.ScanDetailTable{
   592  		Headers: []string{
   593  			"Policy Severity Level", "Number of Components in Violation",
   594  		},
   595  		WithCounter: false,
   596  	}
   597  
   598  	for _, level := range policyStatus.SeverityLevels {
   599  		row := reporting.ScanRow{}
   600  		row.AddColumn(level.Name, 0)
   601  		row.AddColumn(level.Value, 0)
   602  		detailTable.Rows = append(detailTable.Rows, row)
   603  	}
   604  	policyReport.DetailTable = detailTable
   605  
   606  	return policyReport
   607  }
   608  
   609  func writePolicyStatusReports(scanReport reporting.ScanReport, config detectExecuteScanOptions, utils detectUtils) ([]piperutils.Path, error) {
   610  	reportPaths := []piperutils.Path{}
   611  
   612  	htmlReport, _ := scanReport.ToHTML()
   613  	htmlReportPath := "piper_detect_policy_violation_report.html"
   614  	if err := utils.FileWrite(htmlReportPath, htmlReport, 0666); err != nil {
   615  		log.SetErrorCategory(log.ErrorConfiguration)
   616  		return reportPaths, errors.Wrapf(err, "failed to write html report")
   617  	}
   618  	reportPaths = append(reportPaths, piperutils.Path{Name: "BlackDuck Policy Violation Report", Target: htmlReportPath})
   619  
   620  	jsonReport, _ := scanReport.ToJSON()
   621  	if exists, _ := utils.DirExists(reporting.StepReportDirectory); !exists {
   622  		err := utils.MkdirAll(reporting.StepReportDirectory, 0777)
   623  		if err != nil {
   624  			return reportPaths, errors.Wrap(err, "failed to create reporting directory")
   625  		}
   626  	}
   627  	if err := utils.FileWrite(filepath.Join(reporting.StepReportDirectory, fmt.Sprintf("detectExecuteScan_policy_%v.json", fmt.Sprintf("%v", time.Now()))), jsonReport, 0666); err != nil {
   628  		return reportPaths, errors.Wrapf(err, "failed to write json report")
   629  	}
   630  
   631  	return reportPaths, nil
   632  }
   633  
   634  func writeIpPolicyJson(config detectExecuteScanOptions, utils detectUtils, paths []piperutils.Path, sys *blackduckSystem) (error, int) {
   635  	components, err := sys.Client.GetComponentsWithLicensePolicyRule(config.ProjectName, getVersionName(config))
   636  	if err != nil {
   637  		errors.Wrapf(err, "failed to get License Policy Violations")
   638  		return err, 0
   639  	}
   640  
   641  	violationCount := getActivePolicyViolations(components)
   642  	violations := struct {
   643  		PolicyViolations int      `json:"policyViolations"`
   644  		Reports          []string `json:"reports"`
   645  	}{
   646  		PolicyViolations: violationCount,
   647  		Reports:          []string{},
   648  	}
   649  
   650  	for _, path := range paths {
   651  		violations.Reports = append(violations.Reports, path.Target)
   652  	}
   653  	if files, err := utils.Glob("**/*BlackDuck_RiskReport.pdf"); err == nil && len(files) > 0 {
   654  		// there should only be one RiskReport thus only taking the first one
   655  		_, reportFile := filepath.Split(files[0])
   656  		violations.Reports = append(violations.Reports, reportFile)
   657  	}
   658  
   659  	violationContent, err := json.Marshal(violations)
   660  	if err != nil {
   661  		return fmt.Errorf("failed to marshal policy violation data: %w", err), violationCount
   662  	}
   663  
   664  	err = utils.FileWrite("blackduck-ip.json", violationContent, 0666)
   665  	if err != nil {
   666  		return fmt.Errorf("failed to write policy violation report: %w", err), violationCount
   667  	}
   668  	return nil, violationCount
   669  }
   670  
   671  func getActivePolicyViolations(components *bd.Components) int {
   672  	if components.TotalCount == 0 {
   673  		return 0
   674  	}
   675  	activeViolations := 0
   676  	for _, component := range components.Items {
   677  		if isActivePolicyViolation(component.PolicyStatus) {
   678  			activeViolations++
   679  		}
   680  	}
   681  	return activeViolations
   682  }
   683  
   684  func isActivePolicyViolation(status string) bool {
   685  	if status == "IN_VIOLATION" {
   686  		return true
   687  	}
   688  	return false
   689  }
   690  
   691  // create toolrecord file for detectExecute
   692  func createToolRecordDetect(workspace string, config detectExecuteScanOptions, sys *blackduckSystem) (string, error) {
   693  	record := toolrecord.New(workspace, "detectExecute", config.ServerURL)
   694  	project, err := sys.Client.GetProject(config.ProjectName)
   695  	if err != nil {
   696  		return "", fmt.Errorf("TR_DETECT: GetProject failed %v", err)
   697  	}
   698  	metadata := project.Metadata
   699  	projectURL := metadata.Href
   700  	if projectURL == "" {
   701  		return "", fmt.Errorf("TR_DETECT: no project URL")
   702  	}
   703  	// project UUID comes as last part of the URL
   704  	parts := strings.Split(projectURL, "/")
   705  	projectId := parts[len(parts)-1]
   706  	if projectId == "" {
   707  		return "", fmt.Errorf("TR_DETECT: no project id in %v", projectURL)
   708  	}
   709  	err = record.AddKeyData("project",
   710  		projectId,
   711  		config.ProjectName,
   712  		projectURL)
   713  	if err != nil {
   714  		return "", err
   715  	}
   716  	record.AddContext("DetectTools", config.DetectTools)
   717  	err = record.Persist()
   718  	if err != nil {
   719  		return "", err
   720  	}
   721  	return record.GetFileName(), nil
   722  }